Skip to content
Permalink
Browse files
fixed #21 result set depends on multi-query order; added regressions …
…to test 315
  • Loading branch information
Stanislav Klinov authored and klirichek committed Jul 18, 2017
1 parent 72395d9 commit 264523021c6983e0c484c2b81fb8d57ee6d6d0c7
Showing with 174 additions and 40 deletions.
  1. +52 −11 src/sphinx.cpp
  2. +4 −1 src/sphinxint.h
  3. +4 −1 src/sphinxrt.cpp
  4. +5 −3 test/helpers.inc
  5. +82 −9 test/test_315/model.bin
  6. +27 −15 test/test_315/test.xml
@@ -14784,9 +14784,12 @@ bool CSphIndex_VLN::MultiScan ( const CSphQuery * pQuery, CSphQueryResult * pRes
iMaxSchemaIndex = i;
}

CSphVector< const ISphSchema * > dSorterSchemas;
SorterSchemas ( ppSorters, iSorters, iMaxSchemaIndex, dSorterSchemas );

// setup calculations and result schema
CSphQueryContext tCtx ( *pQuery );
if ( !tCtx.SetupCalc ( pResult, ppSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, m_tMva.GetWritePtr(), m_bArenaProhibit ) )
if ( !tCtx.SetupCalc ( pResult, ppSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, m_tMva.GetWritePtr(), m_bArenaProhibit, dSorterSchemas ) )
return false;

// set string pool for string on_sort expression fix up
@@ -16324,8 +16327,23 @@ void CSphQueryContext::BindWeights ( const CSphQuery * pQuery, const CSphSchema
}
}

static ESphEvalStage GetEarliestStage ( ESphEvalStage eStage, const CSphColumnInfo & tIn, const CSphVector<const ISphSchema *> & dSchemas )
{
ARRAY_FOREACH ( iSchema, dSchemas )
{
const ISphSchema * pSchema = dSchemas[iSchema];
const CSphColumnInfo * pCol = pSchema->GetAttr ( tIn.m_sName.cstr() );
if ( !pCol )
continue;

eStage = Min ( eStage, pCol->m_eStage );
}

return eStage;
}

bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema & tInSchema,
const CSphSchema & tSchema, const DWORD * pMvaPool, bool bArenaProhibit )
const CSphSchema & tSchema, const DWORD * pMvaPool, bool bArenaProhibit, const CSphVector<const ISphSchema *> & dInSchemas )
{
m_dCalcFilter.Resize ( 0 );
m_dCalcSort.Resize ( 0 );
@@ -16339,15 +16357,16 @@ bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema &
return false;
}

bool bGotAggregate = false;

// now match everyone
for ( int iIn=0; iIn<tInSchema.GetAttrsCount(); iIn++ )
{
const CSphColumnInfo & tIn = tInSchema.GetAttr(iIn);
bGotAggregate |= ( tIn.m_eAggrFunc!=SPH_AGGR_NONE );

switch ( tIn.m_eStage )
// recalculate stage as sorters set column at earlier stage
// FIXME!!! should we update column?
ESphEvalStage eStage = GetEarliestStage ( tIn.m_eStage, tIn, dInSchemas );

switch ( eStage )
{
case SPH_EVAL_STATIC:
case SPH_EVAL_OVERRIDE:
@@ -16362,7 +16381,7 @@ bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema &
return false;
}

if ( tIn.m_eStage==SPH_EVAL_OVERRIDE )
if ( eStage==SPH_EVAL_OVERRIDE )
{
// override; check for type/size match and dynamic part
if ( tIn.m_eAttrType!=pMy->m_eAttrType
@@ -16395,7 +16414,7 @@ bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema &
if ( !pExpr )
{
pResult->m_sError.SetSprintf ( "INTERNAL ERROR: incoming-schema expression missing evaluator (stage=%d, in=%s)",
(int)tIn.m_eStage, sphDumpAttr(tIn).cstr() );
(int)eStage, sphDumpAttr(tIn).cstr() );
return false;
}

@@ -16409,7 +16428,7 @@ bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema &
tMva.m_bArenaProhibit = bArenaProhibit;
tCalc.m_pExpr->Command ( SPH_EXPR_SET_MVA_POOL, &tMva );

switch ( tIn.m_eStage )
switch ( eStage )
{
case SPH_EVAL_PREFILTER: m_dCalcFilter.Add ( tCalc ); break;
case SPH_EVAL_PRESORT: m_dCalcSort.Add ( tCalc ); break;
@@ -16426,7 +16445,7 @@ bool CSphQueryContext::SetupCalc ( CSphQueryResult * pResult, const ISphSchema &
break;

default:
pResult->m_sError.SetSprintf ( "INTERNAL ERROR: unhandled eval stage=%d", (int)tIn.m_eStage );
pResult->m_sError.SetSprintf ( "INTERNAL ERROR: unhandled eval stage=%d", (int)eStage );
return false;
}
}
@@ -18101,12 +18120,15 @@ bool CSphIndex_VLN::ParsedMultiQuery ( const CSphQuery * pQuery, CSphQueryResult
iMaxSchemaIndex = i;
}

CSphVector< const ISphSchema * > dSorterSchemas;
SorterSchemas ( ppSorters, iSorters, iMaxSchemaIndex, dSorterSchemas );

// setup calculations and result schema
CSphQueryContext tCtx ( *pQuery );
tCtx.m_pProfile = pProfile;
tCtx.m_pLocalDocs = tArgs.m_pLocalDocs;
tCtx.m_iTotalDocs = tArgs.m_iTotalDocs;
if ( !tCtx.SetupCalc ( pResult, ppSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, m_tMva.GetWritePtr(), m_bArenaProhibit ) )
if ( !tCtx.SetupCalc ( pResult, ppSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, m_tMva.GetWritePtr(), m_bArenaProhibit, dSorterSchemas ) )
return false;

// set string pool for string on_sort expression fix up
@@ -32436,6 +32458,25 @@ void sphShutdownGlobalIDFs ()
sphUpdateGlobalIDFs ( dEmptyFiles );
}

//////////////////////////////////////////////////////////////////////////

void SorterSchemas ( ISphMatchSorter ** ppSorters, int iCount, int iSkipSorter, CSphVector<const ISphSchema *> & dSchemas )
{
if ( iCount<2 )
return;

dSchemas.Reserve ( iCount - 1 );
for ( int i=0; i<iCount; i++ )
{
if ( i==iSkipSorter || !ppSorters[i] )
continue;

const ISphSchema & tSchema = ppSorters[i]->GetSchema();
dSchemas.Add ( &tSchema );
}
}


//////////////////////////////////////////////////////////////////////////

//
@@ -483,7 +483,7 @@ class CSphQueryContext : public ISphNoncopyable
~CSphQueryContext ();

void BindWeights ( const CSphQuery * pQuery, const CSphSchema & tSchema, CSphString & sWarning );
bool SetupCalc ( CSphQueryResult * pResult, const ISphSchema & tInSchema, const CSphSchema & tSchema, const DWORD * pMvaPool, bool bArenaProhibit );
bool SetupCalc ( CSphQueryResult * pResult, const ISphSchema & tInSchema, const CSphSchema & tSchema, const DWORD * pMvaPool, bool bArenaProhibit, const CSphVector<const ISphSchema *> & dInSchemas );
bool CreateFilters ( bool bFullscan, const CSphVector<CSphFilterSettings> * pdFilters, const ISphSchema & tSchema, const DWORD * pMvaPool, const BYTE * pStrings, CSphString & sError, CSphString & sWarning, ESphCollation eCollation, bool bArenaProhibit, const KillListVector & dKillList );
bool SetupOverrides ( const CSphQuery * pQuery, CSphQueryResult * pResult, const CSphSchema & tIndexSchema, const ISphSchema & tOutgoingSchema );

@@ -504,6 +504,9 @@ class CSphQueryContext : public ISphNoncopyable
CSphVector<const UservarIntSet_c*> m_dUserVals;
};


void SorterSchemas ( ISphMatchSorter ** ppSorters, int iCount, int iSkipSorter, CSphVector<const ISphSchema *> & dSchemas );

//////////////////////////////////////////////////////////////////////////
// MEMORY TRACKER
//////////////////////////////////////////////////////////////////////////
@@ -7205,10 +7205,13 @@ bool RtIndex_t::MultiQuery ( const CSphQuery * pQuery, CSphQueryResult * pResult
}
}

CSphVector< const ISphSchema * > dSorterSchemas;
SorterSchemas ( dSorters.Begin(), dSorters.GetLength(), iMaxSchemaIndex, dSorterSchemas );

// setup calculations and result schema
CSphQueryContext tCtx ( *pQuery );
tCtx.m_pProfile = pProfiler;
if ( !tCtx.SetupCalc ( pResult, dSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, NULL, false ) )
if ( !tCtx.SetupCalc ( pResult, dSorters[iMaxSchemaIndex]->GetSchema(), m_tSchema, NULL, false, dSorterSchemas ) )
return false;

tCtx.m_uPackedFactorFlags = tArgs.m_uPackedFactorFlags;
@@ -752,16 +752,18 @@ class QLClient
return $res;
}

function MultiQuery($q)
function MultiQuery($q, $opt)
{
if ($this->_conn===false)
return "NOT CONNECTED";

$r = @mysqli_multi_query($this->_conn, $q);
if (!$r)
return "ERROR: ".mysqli_error($this->_conn);

$res = "";
if (array_key_exists("dumpQuery", $opt) && $opt["dumpQuery"] === true)
$res .= $q . "\n---\n";
do
{
if ($result = mysqli_store_result($this->_conn))
@@ -772,7 +774,7 @@ class QLClient
$res .= join(" | ", $row) . "\n";
$n++;
}
$res .= "$n rows";
$res .= "$n rows" . "\n";
mysqli_free_result($result);
} else
{
@@ -1,27 +1,100 @@
a:1:{i:0;a:1:{i:0;a:10:{i:0;s:7:"
a:1:{i:0;a:1:{i:0;a:23:{i:0;s:7:"
insert";i:1;s:3:"
OK";i:2;s:8:"
rt scan";i:3;s:63:"
rt scan";i:3;s:64:"
1 | 1 | 1 | 1
2 | 2 | 1 | 2
3 | 3 | 2 | 3
4 | 4 | 2 | 4
4 rows";i:4;s:9:"
rt match";i:5;s:1407:"
4 rows
";i:4;s:9:"
rt match";i:5;s:1408:"
1 | 1 | 1 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
2 | 2 | 1 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
3 | 3 | 2 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
4 | 4 | 2 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
4 rows";i:6;s:11:"
plain scan";i:7;s:63:"
4 rows
";i:6;s:11:"
plain scan";i:7;s:64:"
1 | 1 | 1 | 1
2 | 2 | 1 | 2
3 | 3 | 2 | 3
4 | 4 | 2 | 4
4 rows";i:8;s:12:"
plain match";i:9;s:1407:"
4 rows
";i:8;s:12:"
plain match";i:9;s:1408:"
1 | 1 | 1 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
2 | 2 | 1 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
3 | 3 | 2 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
4 | 4 | 2 | bm25=304, bm25a=0.168710, field_mask=1, doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1, tf_idf=-0.430677, min_idf=-0.430677, max_idf=-0.430677, sum_idf=-0.430677, min_hit_pos=2, min_best_span_pos=2, exact_hit=0, max_window_hits=1, min_gaps=0, exact_order=1, lccs=1, wlccs=-0.430677, atc=0.000000), word0=(tf=1, idf=-0.430677)
4 rows";}}}
4 rows
";i:10;s:26:"
--- multi-query order ---";i:11;s:8:"
rt scan";i:12;s:212:"
select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by p
---
1 | 0 | 4
1 rows
1 | 0 | 2
1 | 1 | 2
2 rows
";i:13;s:212:"
select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by i
---
1 | 0 | 2
1 | 1 | 2
2 rows
1 | 0 | 4
1 rows
";i:14;s:9:"
rt match";i:15;s:252:"
select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by p
---
1 | 0 | 4
1 rows
1 | 0 | 2
1 | 1 | 2
2 rows
";i:16;s:252:"
select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by i
---
1 | 0 | 2
1 | 1 | 2
2 rows
1 | 0 | 4
1 rows
";i:17;s:11:"
plain scan";i:18;s:206:"
select 1 i, interval(attributes_id, 2) p, count(*) c from products group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from products group by p
---
1 | 0 | 4
1 rows
1 | 0 | 2
1 | 1 | 2
2 rows
";i:19;s:206:"
select 1 i, interval(attributes_id, 2) p, count(*) c from products group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from products group by i
---
1 | 0 | 2
1 | 1 | 2
2 rows
1 | 0 | 4
1 rows
";i:20;s:12:"
plain match";i:21;s:246:"
select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by p
---
1 | 0 | 4
1 rows
1 | 0 | 2
1 | 1 | 2
2 rows
";i:22;s:246:"
select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by i
---
1 | 0 | 2
1 | 1 | 2
2 rows
1 | 0 | 4
1 rows
";}}}
@@ -64,37 +64,49 @@ INSERT INTO test_table (best_seller, attributes_id, text) VALUES
( 4, 2, 'text4 text' );
</db_insert>

<!--
<sphqueries>
<sphinxql>REPLACE INTO rt_products (id, best_seller, attributes_id, title) VALUES (1, 1, 1, 'text1 text'),(2, 2, 1, 'text2 text'), (3, 3, 2, 'text3 text'), (4, 4, 2, 'text4 text')</sphinxql>
<sphinxql>select *, to_string(id) i from rt_products order by best_seller asc; select *, to_string(id) i from rt_products order by best_seller1 asc; select *, to_string(id) i from rt_products order by attributes_id asc</sphinxql>
<sphinxql>select *, packedfactors() i from rt_products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by attributes_id asc option ranker=expr('1')</sphinxql>
<sphinxql>select *, to_string(id) i from products order by best_seller asc; select *, to_string(id) i from products order by best_seller1 asc; select *, to_string(id) i from products order by attributes_id asc</sphinxql>
<sphinxql>select *, packedfactors() i from products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by attributes_id asc option ranker=expr('1')</sphinxql>
</sphqueries>
-->

<custom_test><![CDATA[
// invalid sorter at multi-query
$results = array();
$ql->Reconnect();
$multiOpts = array();
$results[] = "\n" . 'insert';
$results[] = "\n" . $ql->Query ( "INSERT INTO rt_products (id, best_seller, attributes_id, title) VALUES (1, 1, 1, 'text1 text'),(2, 2, 1, 'text2 text'), (3, 3, 2, 'text3 text'), (4, 4, 2, 'text4 text')" );
$results[] = "\n" . 'rt scan';
$results[] = "\n" . $ql->MultiQuery ( "select *, to_string(id) i from rt_products order by best_seller asc; select *, to_string(id) i from rt_products order by best_seller1 asc; select *, to_string(id) i from rt_products order by attributes_id asc" );
$results[] = "\n" . $ql->MultiQuery ( "select *, to_string(id) i from rt_products order by best_seller asc; select *, to_string(id) i from rt_products order by best_seller1 asc; select *, to_string(id) i from rt_products order by attributes_id asc", $multiOpts );
$results[] = "\n" . 'rt match';
$results[] = "\n" . $ql->MultiQuery ( "select *, packedfactors() i from rt_products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by attributes_id asc option ranker=expr('1')", $multiOpts );
$results[] = "\n" . 'plain scan';
$results[] = "\n" . $ql->MultiQuery ( "select *, to_string(id) i from products order by best_seller asc; select *, to_string(id) i from products order by best_seller1 asc; select *, to_string(id) i from products order by attributes_id asc", $multiOpts );
$results[] = "\n" . 'plain match';
$results[] = "\n" . $ql->MultiQuery ( "select *, packedfactors() i from products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by attributes_id asc option ranker=expr('1')", $multiOpts );
///
// multi-query order case
$multiOpts["dumpQuery"] = true;
$results[] = "\n" . '--- multi-query order ---';
$results[] = "\n" . 'rt scan';
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by p", $multiOpts );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products group by i", $multiOpts );
$results[] = "\n" . 'rt match';
$results[] = "\n" . $ql->MultiQuery ( "select *, packedfactors() i from rt_products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from rt_products where match ('text') order by attributes_id asc option ranker=expr('1')" );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by p", $multiOpts );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from rt_products where match('text') group by i", $multiOpts );
$results[] = "\n" . 'plain scan';
$results[] = "\n" . $ql->MultiQuery ( "select *, to_string(id) i from products order by best_seller asc; select *, to_string(id) i from products order by best_seller1 asc; select *, to_string(id) i from products order by attributes_id asc" );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from products group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from products group by p", $multiOpts );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from products group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from products group by i", $multiOpts );
$results[] = "\n" . 'plain match';
$results[] = "\n" . $ql->MultiQuery ( "select *, packedfactors() i from products where match ('text') order by best_seller asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by best_seller1 asc option ranker=expr('1'); select *, packedfactors() i from products where match ('text') order by attributes_id asc option ranker=expr('1')" );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by i; select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by p", $multiOpts );
$results[] = "\n" . $ql->MultiQuery ( "select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by p; select 1 i, interval(attributes_id, 2) p, count(*) c from products where match('text') group by i", $multiOpts );
]]></custom_test>

0 comments on commit 2645230

Please sign in to comment.