[mapguide-commits] r10251 - in branches/4.0/MgDev/Server/src: Gws/GwsQueryEngine Gws/GwsQueryEngine/inc UnitTesting
svn_mapguide at osgeo.org
svn_mapguide at osgeo.org
Mon Nov 2 20:01:25 PST 2026
Author: jng
Date: 2026-11-02 20:01:24 -0800 (Mon, 02 Nov 2026)
New Revision: 10251
Added:
branches/4.0/MgDev/Server/src/UnitTesting/TestGwsQueryEngine.cpp
Modified:
branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFeatureQueryResults.cpp
branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightBatchSortedBlockJoinQueryResults.cpp
branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightNestedLoopSortedBlockJoinQueryResults.cpp
branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/inc/GwsQuery.h
branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
Log:
Fix the batch sorted block join and cover every GwsQueryEngine join algorithm with unit tests
#1888 (r6333/r6348) stopped the engine from choosing eGwsBatchSortedBlock for
SDF/SHP joins rather than fixing the algorithm, which swept the defects behind
#502, #791, #1396 and #1790 under the rug. This change fixes the algorithm for
the cases those tickets describe and adds a provider-free test suite for all
four implemented join methods.
The batch right-hand side used one forward secondary cursor as its matching
contract: it compared the primary key at m_joinKeyIndex against the current
secondary row and assumed the provider's ordering agreed with
GWSFdoUtilities::CompareDataValues. A provider whose collation ordered rows
differently (the case-insensitive "a016" of #502) left the cursor stuck, so
every following primary key was reported unmatched, and because the key index
advanced one step per primary row a repeated primary key could never replay the
row consumed for its predecessor (#1396/#1790). A NULL primary key also produced
a null entry for inner joins and could produce it repeatedly.
* Server/src/Gws/GwsQueryEngine/GwsRightBatchSortedBlockJoinQueryResults.cpp:
the per-block IN query is still issued once, but its result set is now
drained into the right-side pool and the pool slots are sorted by
GWSFdoUtilities::CompareDataValues (stable, so equal secondary keys keep the
provider's order). ReadNext() binary-searches that block for the current
primary key and serves the matching group, so matching no longer depends on
the provider's row order and duplicate primary keys replay their group. A
NULL primary key emits exactly one NULL row for a left outer join and none
for an inner join, and the getters read the pooled feature (index-based
getters map the index through the result descriptor), which is what makes a
drained block readable.
* Server/src/Gws/GwsQueryEngine/inc/GwsQuery.h: the block's key/order/group
state and the inline key-index and NULL-key helpers that reset it.
* Server/src/Gws/GwsQueryEngine/GwsRightNestedLoopSortedBlockJoinQueryResults.cpp:
drain the remaining matches into the pool when SetRelatedValues() is called
again for the same key. The caller has read only the first match at that
point, so reusing the pool without filling it returned one row of an
inner-join one-to-many group and dropped the rest.
* Server/src/Gws/GwsQueryEngine/GwsFeatureQueryResults.cpp:
GetRevisionNumber() C-style-cast m_prepquery to CGwsPreparedFeatureQuery,
but for a join iterator m_prepquery is the prepared join query; pooling a
right-side iterator read m_revisionprop through the wrong type and crashed.
Use a dynamic_cast and return -1 when it is not a feature query.
* Server/src/UnitTesting/TestGwsQueryEngine.cpp (new), together with entries
in Server/src/UnitTesting/CMakeLists.txt, UnitTesting.cpp and
UnitTesting.vcxproj: a [GwsQueryEngine] suite of 17 cases and 404 assertions.
It mocks both join sources as in-memory tables behind CGwsFeatureIterator and
CGwsPreparedQuery subclasses, so no FDO provider or data source is involved,
and drives eGwsSortMerge, eGwsNestedLoops, eGwsNestedLoopSortedBlock and
eGwsBatchSortedBlock through the real CGwsPreparedJoinQuery::Execute() path.
eGwsHash has no prepared-query branch or iterator and is not exercised. The
cases cover inner and left outer joins, one-to-one, one-to-many (duplicate
secondary keys), many-to-one (duplicate primary keys), gaps, string keys,
multi-column joins, NULL keys, empty sources, ForceOneToOne, unsorted primary
input, a secondary order that disagrees with the engine's comparison, a
provider collation that differs from wcscmp, multi-batch blocks
(sm_nBatchSize = 2) and 40-row tables. A query-count case asserts that the
batch join executes the secondary once per block rather than once per primary
row, which is the #791 behaviour this algorithm exists for.
Verified on ubuntu22 x64 in the Debug/ASan tree: a full incremental server build
is clean, and mgserver test GwsQueryEngine reports "All tests passed (404
assertions in 17 test cases)" with no LeakSanitizer records; three consecutive
runs were identical, and the container test runner reports the component suite
passed.
Fixes #2896
Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFeatureQueryResults.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFeatureQueryResults.cpp 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFeatureQueryResults.cpp 2026-11-03 04:01:24 UTC (rev 10251)
@@ -205,11 +205,13 @@
FdoInt32 CGwsFeatureIterator::GetRevisionNumber ()
{
- if (m_reader == NULL)
+ // m_prepquery is not always a feature query: for a join iterator it is the
+ // prepared join query. Only a feature query knows the revision property.
+ CGwsPreparedFeatureQuery * pquery = dynamic_cast<CGwsPreparedFeatureQuery *>(m_prepquery);
+ if (m_reader == NULL || pquery == NULL)
return -1;
- const WSTR & revnumber =
- ((CGwsPreparedFeatureQuery *)m_prepquery)->m_revisionprop;
+ const WSTR & revnumber = pquery->m_revisionprop;
if (revnumber.empty())
return -1;
Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightBatchSortedBlockJoinQueryResults.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightBatchSortedBlockJoinQueryResults.cpp 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightBatchSortedBlockJoinQueryResults.cpp 2026-11-03 04:01:24 UTC (rev 10251)
@@ -24,6 +24,7 @@
#include "stdafx.h"
#include "GwsQueryEngineImp.h"
+#include <algorithm>
/////////////////////////////////////////////////////////////////////
//
@@ -31,22 +32,153 @@
//
/////////////////////////////////////////////////////////////////////
+namespace
+{
+ // Orders pool slots by the join key using the same comparison the join
+ // predicates use, rather than assuming the provider returned them in that order.
+ struct BatchBlockKeyLess
+ {
+ const std::vector<FdoDataValue*>* Keys;
+
+ bool operator()(int left, int right) const
+ {
+ return GWSFdoUtilities::CompareDataValues((*Keys)[left], (*Keys)[right]) < 0;
+ }
+ };
+}
+
+
CGwsRightBatchSortedBlockJoinQueryResults::CGwsRightBatchSortedBlockJoinQueryResults ()
{
- m_pos = eBeforeFirstRow;
m_joinKeyIndex = 0;
- m_bMoreData = true;
m_bNullEntry = false;
m_bPrimaryKeyNull = false;
- m_bFirstReadNext = true;
+ m_nullEntryEmitted = false;
+ m_queryType = eGwsQueryEqualJoin;
+ m_bOneToOneJoin = false;
+ m_dtSecondary = FdoDataType_String;
+ m_propName = NULL;
+ m_groupKeyIndex = -1;
+ m_groupBegin = 0;
+ m_groupEnd = 0;
+ m_groupPos = 0;
}
CGwsRightBatchSortedBlockJoinQueryResults::~CGwsRightBatchSortedBlockJoinQueryResults () throw()
{
+ ClearBlock();
}
+void CGwsRightBatchSortedBlockJoinQueryResults::ClearBlock()
+{
+ for (size_t i = 0; i < m_blockKeys.size(); ++i)
+ FDO_SAFE_RELEASE(m_blockKeys[i]);
+
+ m_blockKeys.clear();
+ m_blockOrder.clear();
+ m_groupKeyIndex = -1;
+ m_groupBegin = 0;
+ m_groupEnd = 0;
+ m_groupPos = 0;
+ m_usepool = false;
+ m_poolpos = -1;
+}
+
+
+void CGwsRightBatchSortedBlockJoinQueryResults::BuildBlock()
+{
+ m_pool->Reset();
+ m_usepool = false;
+ m_poolpos = -1;
+
+ // Drain the batch's secondary query once. The pool owns a copy of every row so
+ // that a primary key can be matched again later in the batch (duplicate primary
+ // keys, or a secondary row the provider ordered inconsistently with our own
+ // comparison).
+ while (CGwsRightJoinQueryResults::ReadNext())
+ {
+ FdoDataValue* key = NULL;
+ if (m_reader->IsNull(m_propName))
+ key = FdoDataValue::Create(m_dtSecondary);
+ else
+ key = GetSecondaryDataValue(m_dtSecondary, m_propName);
+
+ m_blockKeys.push_back(key);
+ m_pool->AddFeature(this);
+ }
+
+ m_blockOrder.resize(m_blockKeys.size());
+ for (size_t i = 0; i < m_blockOrder.size(); ++i)
+ m_blockOrder[i] = static_cast<int>(i);
+
+ BatchBlockKeyLess less;
+ less.Keys = &m_blockKeys;
+ std::stable_sort(m_blockOrder.begin(), m_blockOrder.end(), less);
+}
+
+
+bool CGwsRightBatchSortedBlockJoinQueryResults::PositionGroupForCurrentKey()
+{
+ if (m_groupKeyIndex == m_joinKeyIndex)
+ return m_groupPos < m_groupEnd;
+
+ m_groupKeyIndex = m_joinKeyIndex;
+ m_groupBegin = 0;
+ m_groupEnd = 0;
+ m_groupPos = 0;
+
+ if (m_joinKeyIndex < 0 || m_joinKeyIndex >= m_joinkeys.GetCount() || m_blockOrder.empty())
+ return false;
+
+ FdoPtr<FdoDataValue> key = m_joinkeys.GetItem(m_joinKeyIndex);
+ if (key == NULL || key->IsNull())
+ return false;
+
+ const int count = static_cast<int>(m_blockOrder.size());
+ int lo = 0;
+ int hi = count;
+ while (lo < hi)
+ {
+ int mid = (lo + hi) / 2;
+ if (GWSFdoUtilities::CompareDataValues(m_blockKeys[m_blockOrder[mid]], key) < 0)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ m_groupBegin = lo;
+
+ hi = count;
+ while (lo < hi)
+ {
+ int mid = (lo + hi) / 2;
+ if (GWSFdoUtilities::CompareDataValues(m_blockKeys[m_blockOrder[mid]], key) <= 0)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ m_groupEnd = lo;
+ m_groupPos = m_groupBegin;
+
+ return m_groupPos < m_groupEnd;
+}
+
+
+FdoString* CGwsRightBatchSortedBlockJoinQueryResults::PropertyNameAt(FdoInt32 index)
+{
+ FdoPtr<IGWSExtendedFeatureDescription> descriptor;
+ DescribeFeature(&descriptor);
+
+ FdoPtr<FdoStringCollection> names = descriptor->PropertyNames();
+ if (index < 0 || index >= names->GetCount())
+ return NULL;
+
+ m_indexPropertyName = names->GetString(index);
+ return const_cast<FdoString*>(m_indexPropertyName.c_str());
+}
+
+
EGwsStatus CGwsRightBatchSortedBlockJoinQueryResults::InitializeReader (
IGWSQuery * query,
CGwsPreparedQuery * prepquery,
@@ -77,6 +209,15 @@
m_bOneToOneJoin = extFeatDsc->ForceOneToOneJoin();
}
+ m_joinKeyIndex = 0;
+ m_groupKeyIndex = -1;
+ m_groupBegin = 0;
+ m_groupEnd = 0;
+ m_groupPos = 0;
+ m_bNullEntry = false;
+ m_bPrimaryKeyNull = false;
+ m_nullEntryEmitted = false;
+
return status;
}
@@ -83,250 +224,28 @@
bool CGwsRightBatchSortedBlockJoinQueryResults::ReadNext()
{
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf(" ReadNext() - Start\n");
- printf(" ReadNext() - JoinColumns:%S\n", m_joincols->ToString());
- wchar_t buffer[2048];
- m_joinkeys.ToString(buffer, 2048);
- printf(" ReadNext() - JoinKeys:\n%S\n", buffer);
- #endif
-
- // Check if primary key is NULL
- if(m_bPrimaryKeyNull)
- {
- m_bNullEntry = true;
- return true;
- }
-
- // Advance the secondary
- bool bRet = false;
- bool bAddNull = false;
m_bNullEntry = false;
- if(m_bMoreData)
+ // A NULL primary key matches nothing. A left outer join reports one NULL row for
+ // it; an inner join drops it.
+ if (m_bPrimaryKeyNull)
{
- bool bDone = false;
- while(!bDone)
- {
- if(eBeforeFirstRow == m_pos)
- {
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf(" ReadNext() - Calling secondary ReadNext()\n");
- #endif
- bRet = CGwsRightJoinQueryResults::ReadNext();
- if(!bRet)
- {
- // No more data
- m_bMoreData = false;
- }
- }
- else
- {
- bRet = true;
- }
+ if (m_queryType != eGwsQueryLeftOuterJoin || m_nullEntryEmitted)
+ return false;
- if(bRet)
- {
- bool bIsNullResult = m_reader->IsNull(m_propName);
- if(!bIsNullResult)
- {
- FdoPtr<FdoDataValue> dataValuePrimary = (FdoDataValue*)m_joinkeys.GetItem(m_joinKeyIndex);
- FdoPtr<FdoDataValue> dataValueSecondary = GetSecondaryDataValue(m_dtSecondary, m_propName);
- int compare = GWSFdoUtilities::CompareDataValues(dataValuePrimary, dataValueSecondary);
-
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf("*** Comparing - KV:%S, S:%S = %d ***\n", dataValuePrimary->ToString(), dataValueSecondary->ToString(), compare);
- #endif
-
- if(compare == 0) // (keyValue == secondary)
- {
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf("*** MATCH - KV:%S = S:%S ***\n", dataValuePrimary->ToString(), dataValueSecondary->ToString());
- #endif
-
- bDone = true;
-
- // What about one-to-one and one-to-many?
- m_pos = eBeforeFirstRow;
- bRet = true;
- }
- else if(compare == -1) // (keyValue < secondary)
- {
- // No Match
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf("*** NO MATCH - KV:%S < S:%S ***\n", dataValuePrimary->ToString(), dataValueSecondary->ToString());
- #endif
-
- // Secondary key is after primary key and thus requires the primary to be advanced.
- // ie: Primary Key = "CA"
- // Secondardy Key = "CD"
- // "CA" < "CD"
- //
- // However, we don't want to lose this data in the secondary so we want to not advance the secondary reader
-
- // Check the primary query type to see if this is an Outer or Inner join
- if (m_queryType == eGwsQueryLeftOuterJoin)
- {
- // Outer Join
- if(eAfterJoinRow == m_pos)
- {
- if(m_bOneToOneJoin)
- {
- bAddNull = true;
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = true;
- }
- else
- {
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = false;
- }
- }
- else if(eBeforeFirstRow == m_pos)
- {
- if(m_bOneToOneJoin)
- {
- bAddNull = true;
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = true;
- }
- else
- {
- if(m_bFirstReadNext)
- {
- m_pos = eAfterJoinRow;
- bAddNull = true;
- bDone = true;
- bRet = true;
- }
- else
- {
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = false;
- }
- }
- }
- else
- {
- // No match for secondary so add NULL secondary
- bAddNull = true;
- m_pos = eAfterJoinRow;
- bDone = true;
- bRet = true;
- }
- }
- else
- {
- // Inner Join
- // Skip primary as there is no secondary
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = false;
- }
- }
- else
- {
- // No Match
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf("*** NO MATCH - KV:%S != S:%S ***\n", dataValuePrimary->ToString(), dataValueSecondary->ToString());
- #endif
-
- // We want to continue reading from the secondary if we can
- m_pos = eBeforeFirstRow;
- }
- }
- else
- {
- // NULL property
- bAddNull = true;
- }
- }
- else
- {
- // No more records
- if (m_queryType == eGwsQueryLeftOuterJoin)
- {
- if(m_bOneToOneJoin)
- {
- bAddNull = true;
- m_pos = eAfterJoinRow;
- bDone = true;
- bRet = true;
- }
- else
- {
- if(m_bFirstReadNext)
- {
- bAddNull = true;
- bDone = true;
- bRet = true;
- }
- else
- {
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = false;
- }
- }
- }
- else
- {
- m_pos = eOnJoinRow;
- bDone = true;
- bRet = false;
- }
- }
- }
- }
- else
- {
- // If there is no more data from the secondary and it is an outer join add a NULL
- if (m_queryType == eGwsQueryLeftOuterJoin)
- {
- if(m_bOneToOneJoin)
- {
- bAddNull = true;
- bRet = true;
- }
- else
- {
- if(eBeforeFirstRow == m_pos)
- {
- m_pos = eOnJoinRow;
- bRet = false;
- }
- else
- {
- m_pos = eBeforeFirstRow;
- bAddNull = true;
- }
- }
- }
- }
-
- if(bAddNull)
- {
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf("*** Adding NULL secondary ***\n");
- #endif
+ m_nullEntryEmitted = true;
m_bNullEntry = true;
- bRet = true;
+ return true;
}
- if(m_bFirstReadNext)
- {
- m_bFirstReadNext = false;
- }
+ if (!PositionGroupForCurrentKey())
+ return false;
- #ifdef _DEBUG_BATCHSORT_JOIN
- printf(" ReadNext() End - JKI:%d POS:%d Return:%d\n", m_joinKeyIndex, m_pos, bRet);
- #endif
+ m_poolpos = m_blockOrder[m_groupPos];
+ m_usepool = true;
+ m_groupPos++;
- return bRet;
+ return true;
}
@@ -353,6 +272,9 @@
try {
Close ();
+ ClearBlock();
+ m_bNullEntry = false;
+ m_nullEntryEmitted = false;
WSTR filter = L"\"";
@@ -373,8 +295,13 @@
}
FdoPtr<FdoDataValue> val = vals.GetItem (idx);
- FdoStringP valStr = val->ToString();
- filter += valStr;
+ if (val == NULL)
+ filter += L"NULL";
+ else
+ {
+ FdoStringP valStr = val->ToString();
+ filter += valStr;
+ }
}
filter += L")"; // NOXLATE
@@ -398,11 +325,15 @@
m_bClosed = false;
stat = CGwsRightJoinQueryResults::SetRelatedValues (vals);
+ if (IGWSException::IsError (stat)) {
+ return stat;
+ }
- // Reset flag
- m_bMoreData = true;
- m_pos = eBeforeFirstRow;
+ BuildBlock();
+ m_bNullEntry = false;
+ m_nullEntryEmitted = false;
+
} catch (FdoException *e) {
PushFdoException (eGwsFailed, e);
e->Release();
@@ -492,386 +423,450 @@
bool CGwsRightBatchSortedBlockJoinQueryResults::IsNull (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
return true;
}
- else
- {
- return CGwsFeatureIterator::IsNull (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::IsNull (propertyName);
}
-bool CGwsRightBatchSortedBlockJoinQueryResults::IsNull (FdoInt32 index)
+FdoString * CGwsRightBatchSortedBlockJoinQueryResults::GetString (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
- return true;
+ GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::IsNull (index);
- }
+
+ return CGwsRightJoinQueryResults::GetString (propertyName);
}
-FdoString * CGwsRightBatchSortedBlockJoinQueryResults::GetString (FdoString * propertyName)
+bool CGwsRightBatchSortedBlockJoinQueryResults::GetBoolean (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetString (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetBoolean (propertyName);
}
-FdoString * CGwsRightBatchSortedBlockJoinQueryResults::GetString (FdoInt32 index)
+FdoByte CGwsRightBatchSortedBlockJoinQueryResults::GetByte (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetString (index);
- }
+
+ return CGwsRightJoinQueryResults::GetByte (propertyName);
}
-bool CGwsRightBatchSortedBlockJoinQueryResults::GetBoolean (FdoString* propertyName)
+FdoDateTime CGwsRightBatchSortedBlockJoinQueryResults::GetDateTime (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetBoolean (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetDateTime (propertyName);
}
-bool CGwsRightBatchSortedBlockJoinQueryResults::GetBoolean (FdoInt32 index)
+double CGwsRightBatchSortedBlockJoinQueryResults::GetDouble (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetBoolean (index);
- }
+
+ return CGwsRightJoinQueryResults::GetDouble (propertyName);
}
-FdoByte CGwsRightBatchSortedBlockJoinQueryResults::GetByte (FdoString* propertyName)
+FdoInt16 CGwsRightBatchSortedBlockJoinQueryResults::GetInt16 (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetByte (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetInt16 (propertyName);
}
-FdoByte CGwsRightBatchSortedBlockJoinQueryResults::GetByte (FdoInt32 index)
+FdoInt32 CGwsRightBatchSortedBlockJoinQueryResults::GetInt32 (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetByte (index);
- }
+
+ return CGwsRightJoinQueryResults::GetInt32 (propertyName);
}
-FdoDateTime CGwsRightBatchSortedBlockJoinQueryResults::GetDateTime (FdoString* propertyName)
+FdoInt64 CGwsRightBatchSortedBlockJoinQueryResults::GetInt64 (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetDateTime (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetInt64 (propertyName);
}
-FdoDateTime CGwsRightBatchSortedBlockJoinQueryResults::GetDateTime (FdoInt32 index)
+float CGwsRightBatchSortedBlockJoinQueryResults::GetSingle (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetDateTime (index);
- }
+
+ return CGwsRightJoinQueryResults::GetSingle (propertyName);
}
-double CGwsRightBatchSortedBlockJoinQueryResults::GetDouble (FdoString* propertyName)
+FdoLOBValue* CGwsRightBatchSortedBlockJoinQueryResults::GetLOB (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetDouble (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetLOB (propertyName);
}
-double CGwsRightBatchSortedBlockJoinQueryResults::GetDouble (FdoInt32 index)
+FdoIStreamReader* CGwsRightBatchSortedBlockJoinQueryResults::GetLOBStreamReader (const wchar_t* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetDouble (index);
- }
+
+ return CGwsRightJoinQueryResults::GetLOBStreamReader (propertyName);
}
-FdoInt16 CGwsRightBatchSortedBlockJoinQueryResults::GetInt16 (FdoString* propertyName)
+FdoIRaster* CGwsRightBatchSortedBlockJoinQueryResults::GetRaster (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetInt16 (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetRaster (propertyName);
}
-FdoInt16 CGwsRightBatchSortedBlockJoinQueryResults::GetInt16 (FdoInt32 index)
+const FdoByte * CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoString* propertyName, FdoInt32 * count)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetInt16 (index);
- }
+
+ return CGwsRightJoinQueryResults::GetGeometry (propertyName, count);
}
-FdoInt32 CGwsRightBatchSortedBlockJoinQueryResults::GetInt32 (FdoString* propertyName)
+FdoByteArray* CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetInt32 (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetGeometry (propertyName);
}
-FdoInt32 CGwsRightBatchSortedBlockJoinQueryResults::GetInt32 (FdoInt32 index)
+FdoIFeatureReader* CGwsRightBatchSortedBlockJoinQueryResults::GetFeatureObject (FdoString* propertyName)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetInt32 (index);
- }
+
+ return CGwsRightJoinQueryResults::GetFeatureObject (propertyName);
}
-FdoInt64 CGwsRightBatchSortedBlockJoinQueryResults::GetInt64 (FdoString* propertyName)
+bool CGwsRightBatchSortedBlockJoinQueryResults::IsNull (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
- GWS_THROW(eGwsFailed);
+ return true;
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetInt64 (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return IsNull(name);
}
+
+ return CGwsFeatureIterator::IsNull (index);
}
-FdoInt64 CGwsRightBatchSortedBlockJoinQueryResults::GetInt64 (FdoInt32 index)
+FdoString * CGwsRightBatchSortedBlockJoinQueryResults::GetString (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetInt64 (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetString(name);
}
+
+ return CGwsFeatureIterator::GetString (index);
}
-float CGwsRightBatchSortedBlockJoinQueryResults::GetSingle (FdoString* propertyName)
+bool CGwsRightBatchSortedBlockJoinQueryResults::GetBoolean (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetSingle (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetBoolean(name);
}
+
+ return CGwsFeatureIterator::GetBoolean (index);
}
-float CGwsRightBatchSortedBlockJoinQueryResults::GetSingle (FdoInt32 index)
+FdoByte CGwsRightBatchSortedBlockJoinQueryResults::GetByte (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetSingle (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetByte(name);
}
+
+ return CGwsFeatureIterator::GetByte (index);
}
-FdoLOBValue* CGwsRightBatchSortedBlockJoinQueryResults::GetLOB (FdoString* propertyName)
+FdoDateTime CGwsRightBatchSortedBlockJoinQueryResults::GetDateTime (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetLOB (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetDateTime(name);
}
+
+ return CGwsFeatureIterator::GetDateTime (index);
}
-FdoLOBValue* CGwsRightBatchSortedBlockJoinQueryResults::GetLOB (FdoInt32 index)
+double CGwsRightBatchSortedBlockJoinQueryResults::GetDouble (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetLOB (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetDouble(name);
}
+
+ return CGwsFeatureIterator::GetDouble (index);
}
-FdoIStreamReader* CGwsRightBatchSortedBlockJoinQueryResults::GetLOBStreamReader(const wchar_t* propertyName )
+FdoInt16 CGwsRightBatchSortedBlockJoinQueryResults::GetInt16 (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetLOBStreamReader (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetInt16(name);
}
+
+ return CGwsFeatureIterator::GetInt16 (index);
}
-FdoIStreamReader* CGwsRightBatchSortedBlockJoinQueryResults::GetLOBStreamReader(FdoInt32 index)
+FdoInt32 CGwsRightBatchSortedBlockJoinQueryResults::GetInt32 (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetLOBStreamReader (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetInt32(name);
}
+
+ return CGwsFeatureIterator::GetInt32 (index);
}
-FdoIRaster* CGwsRightBatchSortedBlockJoinQueryResults::GetRaster (FdoString* propertyName)
+FdoInt64 CGwsRightBatchSortedBlockJoinQueryResults::GetInt64 (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetRaster (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetInt64(name);
}
+
+ return CGwsFeatureIterator::GetInt64 (index);
}
-FdoIRaster* CGwsRightBatchSortedBlockJoinQueryResults::GetRaster (FdoInt32 index)
+float CGwsRightBatchSortedBlockJoinQueryResults::GetSingle (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetRaster (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetSingle(name);
}
+
+ return CGwsFeatureIterator::GetSingle (index);
}
-const FdoByte * CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoString* propertyName, FdoInt32 * count)
+FdoLOBValue* CGwsRightBatchSortedBlockJoinQueryResults::GetLOB (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetGeometry (propertyName, count);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetLOB(name);
}
+
+ return CGwsFeatureIterator::GetLOB (index);
}
-const FdoByte * CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoInt32 index, FdoInt32 * count)
+FdoIStreamReader* CGwsRightBatchSortedBlockJoinQueryResults::GetLOBStreamReader (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetGeometry (index, count);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetLOBStreamReader(name);
}
+
+ return CGwsFeatureIterator::GetLOBStreamReader (index);
}
-FdoByteArray* CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoString* propertyName)
+FdoIRaster* CGwsRightBatchSortedBlockJoinQueryResults::GetRaster (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetGeometry (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetRaster(name);
}
+
+ return CGwsFeatureIterator::GetRaster (index);
}
-FdoByteArray* CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoInt32 index)
+const FdoByte * CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoInt32 index, FdoInt32 * count)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetGeometry (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetGeometry(name, count);
}
+
+ return CGwsFeatureIterator::GetGeometry (index, count);
}
-FdoIFeatureReader* CGwsRightBatchSortedBlockJoinQueryResults::GetFeatureObject(FdoString* propertyName)
+FdoByteArray* CGwsRightBatchSortedBlockJoinQueryResults::GetGeometry (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetFeatureObject (propertyName);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetGeometry(name);
}
+
+ return CGwsFeatureIterator::GetGeometry (index);
}
-FdoIFeatureReader* CGwsRightBatchSortedBlockJoinQueryResults::GetFeatureObject(FdoInt32 index)
+FdoIFeatureReader* CGwsRightBatchSortedBlockJoinQueryResults::GetFeatureObject (FdoInt32 index)
{
- if(m_bNullEntry)
+ if (m_bNullEntry)
{
GWS_THROW(eGwsFailed);
}
- else
+
+ if (m_usepool)
{
- return CGwsFeatureIterator::GetFeatureObject (index);
+ FdoString* name = PropertyNameAt(index);
+ if (name == NULL)
+ GWS_THROW(eGwsIndexOutOfBounds);
+ return GetFeatureObject(name);
}
+
+ return CGwsFeatureIterator::GetFeatureObject (index);
}
FdoDataValue * CGwsRightBatchSortedBlockJoinQueryResults::GetDataValue (FdoString* propertyName)
@@ -880,10 +875,8 @@
{
GWS_THROW(eGwsFailed);
}
- else
- {
- return CGwsFeatureIterator::GetDataValue (propertyName);
- }
+
+ return CGwsRightJoinQueryResults::GetDataValue (propertyName);
}
@@ -895,24 +888,19 @@
FdoClassDefinition* CGwsRightBatchSortedBlockJoinQueryResults::GetClassDefinition()
{
- FdoClassDefinition* classDef = NULL;
-
if(m_bNullEntry)
{
- // If this is a NULL entry what FdoClassDefinition do we add?
- // We cannot return NULL
- if (m_prepquery)
- {
- FdoPtr<CGwsQueryResultDescriptors> extFeatDsc;
- m_prepquery->DescribeResults((IGWSExtendedFeatureDescription**)&extFeatDsc);
+ FdoPtr<IGWSExtendedFeatureDescription> descriptor;
+ DescribeFeature(&descriptor);
+ return descriptor->ClassDefinition();
+ }
- classDef = extFeatDsc->ClassDefinition();
- }
- }
- else
+ if(m_usepool)
{
- classDef = CGwsFeatureIterator::GetClassDefinition();
+ FdoPtr<IGWSExtendedFeatureDescription> descriptor;
+ DescribeFeature(&descriptor);
+ return descriptor->ClassDefinition();
}
- return classDef;
+ return CGwsFeatureIterator::GetClassDefinition();
}
Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightNestedLoopSortedBlockJoinQueryResults.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightNestedLoopSortedBlockJoinQueryResults.cpp 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsRightNestedLoopSortedBlockJoinQueryResults.cpp 2026-11-03 04:01:24 UTC (rev 10251)
@@ -81,6 +81,12 @@
)
{
if (m_joinkeys == vals) {
+ // The current key was already executed. Drain the remaining matching rows
+ // into the pool before switching to it: the caller has only read the first
+ // match so far, so reusing the pool without filling it would drop the rest
+ // of a one-to-many join.
+ while (ReadNext ())
+ ;
m_usepool = true;
m_poolpos = -1;
return eGwsOk;
Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/inc/GwsQuery.h
===================================================================
--- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/inc/GwsQuery.h 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/inc/GwsQuery.h 2026-11-03 04:01:24 UTC (rev 10251)
@@ -1542,22 +1542,37 @@
virtual FdoClassDefinition* GetClassDefinition();
void IncrementKeyIndex() { m_joinKeyIndex++; }
- void ResetKeyIndex() { m_joinKeyIndex=0; }
- void SetPrimaryKeyNull(bool bValue) { m_bPrimaryKeyNull=bValue; }
+ void ResetKeyIndex() { m_joinKeyIndex=0; m_groupKeyIndex=-1; }
+ void SetPrimaryKeyNull(bool bValue) { m_bPrimaryKeyNull=bValue; m_nullEntryEmitted=false; }
protected:
FdoDataValue* GetSecondaryDataValue(FdoDataType dtSecondary, FdoString* propname);
- EGwsCursorPosition m_pos;
+ void ClearBlock();
+ void BuildBlock();
+ bool PositionGroupForCurrentKey();
+ FdoString* PropertyNameAt(FdoInt32 index);
+
int m_joinKeyIndex;
- bool m_bMoreData;
bool m_bNullEntry;
bool m_bPrimaryKeyNull;
+ bool m_nullEntryEmitted;
EGwsQueryType m_queryType;
bool m_bOneToOneJoin;
FdoDataType m_dtSecondary;
FdoString* m_propName;
- bool m_bFirstReadNext;
+
+ // The secondary rows of the current batch, materialised into m_pool. The keys
+ // are kept per pool slot (m_blockKeys) and m_blockOrder is the slot order
+ // sorted by the engine's own key comparison, so each primary key can be looked
+ // up by exact match instead of relying on the provider's row order.
+ std::vector<FdoDataValue*> m_blockKeys;
+ std::vector<int> m_blockOrder;
+ int m_groupKeyIndex;
+ int m_groupBegin;
+ int m_groupEnd;
+ int m_groupPos;
+ std::wstring m_indexPropertyName;
};
Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-11-03 04:01:24 UTC (rev 10251)
@@ -19,7 +19,11 @@
${MG_SERVER_SERVICES_DIR}/Site
${MG_SERVER_SERVICES_DIR}/Kml
${MG_SERVER_SERVICES_DIR}/Tile
+ ${MG_SERVER_GWS_DIR}/Include
+ ${MG_SERVER_GWS_DIR}/GwsCommon/inc
+ ${MG_SERVER_GWS_DIR}/GwsQueryEngine/inc
${FDO_INCLUDE_DIR}
+ ${FDO_INCLUDE_DIR}/ExpressionEngine
)
set(MgUnitTesting_SRCS
@@ -27,6 +31,7 @@
TestDrawingService.cpp
TestFeatureService.cpp
TestGeometry.cpp
+ TestGwsQueryEngine.cpp
TestKmlService.cpp
TestLogManager.cpp
TestLogManagerThread.cpp
@@ -56,6 +61,10 @@
target_link_libraries(MgUnitTesting
${CPPUNIT_LIBRARIES}
+ ${FDO_LIBRARY}
+ ${FDO_EXPRESSIONENGINE_LIBRARY}
+ MgGwsCommon${MG_VERSION_SUFFIX}
+ MgGwsQueryEngine${MG_VERSION_SUFFIX}
MgRenderers${MG_VERSION_SUFFIX}
MgStylization${MG_VERSION_SUFFIX}
MgMdfModel${MG_VERSION_SUFFIX}
Added: branches/4.0/MgDev/Server/src/UnitTesting/TestGwsQueryEngine.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestGwsQueryEngine.cpp (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestGwsQueryEngine.cpp 2026-11-03 04:01:24 UTC (rev 10251)
@@ -0,0 +1,1440 @@
+//
+// Copyright (C) 2004-2024 by Autodesk, Inc.
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of version 2.1 of the GNU Lesser
+// General Public License as published by the Free Software Foundation.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+// Tests for the GwsQueryEngine join algorithms.
+//
+// The join results iterators (CGwsSortMergeJoinQueryResults and friends) merge a
+// left-hand feature iterator with a right-hand one. In production those iterators
+// wrap an FdoIFeatureReader created by a provider, but nothing in the merge logic
+// needs a real provider: a CGwsFeatureIterator subclass over an in-memory table
+// and a CGwsPreparedQuery subclass that hands those iterators out are enough.
+// The tests below drive every implemented join method (eGwsSortMerge,
+// eGwsNestedLoops, eGwsNestedLoopSortedBlock and eGwsBatchSortedBlock) through
+// that harness and assert the merged rows. eGwsHash is declared in
+// EGwsJoinMethod but has neither a prepared-query branch nor an iterator in
+// CGwsPreparedQuery::CreateFeatureIterator, so there is nothing to exercise.
+//
+// eGwsBatchSortedBlock is selected when the primary source cannot order its
+// results (the SDF/SHP case behind tickets #502, #791, #1396 and #1790). It is
+// covered here, including its multi-batch and query-count behaviour and the
+// cases that used to reproduce those tickets (duplicate primary keys and a
+// provider collation that differs from the engine's key comparison).
+
+#include "GwsQueryEngineImp.h"
+
+#include "catch.hpp"
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+namespace
+{
+ std::string Narrow(const std::wstring& value)
+ {
+ return std::string(value.begin(), value.end());
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // In-memory feature source
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ struct FakeValue
+ {
+ FakeValue() : Type(FdoDataType_String), IsNullValue(true), IntValue(0), DoubleValue(0.0) {}
+
+ static FakeValue Int32(FdoInt32 value)
+ {
+ FakeValue v;
+ v.Type = FdoDataType_Int32;
+ v.IsNullValue = false;
+ v.IntValue = value;
+ return v;
+ }
+
+ static FakeValue Int64(FdoInt64 value)
+ {
+ FakeValue v;
+ v.Type = FdoDataType_Int64;
+ v.IsNullValue = false;
+ v.IntValue = value;
+ return v;
+ }
+
+ static FakeValue Double(double value)
+ {
+ FakeValue v;
+ v.Type = FdoDataType_Double;
+ v.IsNullValue = false;
+ v.DoubleValue = value;
+ return v;
+ }
+
+ static FakeValue String(const wchar_t* value)
+ {
+ FakeValue v;
+ v.Type = FdoDataType_String;
+ v.IsNullValue = false;
+ v.StringValue = value;
+ return v;
+ }
+
+ static FakeValue Null(FdoDataType type)
+ {
+ FakeValue v;
+ v.Type = type;
+ v.IsNullValue = true;
+ return v;
+ }
+
+ FdoDataValue* CreateDataValue() const
+ {
+ if (IsNullValue)
+ return FdoDataValue::Create(Type);
+
+ switch (Type)
+ {
+ case FdoDataType_Int32:
+ return FdoDataValue::Create(static_cast<FdoInt32>(IntValue));
+ case FdoDataType_Int64:
+ return FdoDataValue::Create(static_cast<FdoInt64>(IntValue));
+ case FdoDataType_Double:
+ return FdoDataValue::Create(DoubleValue, FdoDataType_Double);
+ case FdoDataType_String:
+ return FdoDataValue::Create(StringValue.c_str());
+ default:
+ return FdoDataValue::Create(Type);
+ }
+ }
+
+ std::wstring AsText() const
+ {
+ if (IsNullValue)
+ return std::wstring();
+
+ switch (Type)
+ {
+ case FdoDataType_Int32:
+ case FdoDataType_Int64:
+ return std::to_wstring(IntValue);
+ case FdoDataType_Double:
+ return std::to_wstring(DoubleValue);
+ case FdoDataType_String:
+ return StringValue;
+ default:
+ return std::wstring();
+ }
+ }
+
+ FdoDataType Type;
+ bool IsNullValue;
+ FdoInt64 IntValue;
+ double DoubleValue;
+ std::wstring StringValue;
+ };
+
+ struct FakeColumn
+ {
+ FakeColumn(const wchar_t* name, FdoDataType type, bool identity)
+ : Name(name), Type(type), IsIdentity(identity)
+ {
+ }
+
+ std::wstring Name;
+ FdoDataType Type;
+ bool IsIdentity;
+ };
+
+ struct FakeTable
+ {
+ FakeTable() : CaseInsensitiveFilter(false) {}
+
+ std::wstring ClassName;
+ std::vector<FakeColumn> Columns;
+ std::vector<std::vector<FakeValue> > Rows;
+
+ // Models a provider whose collation is case-insensitive: it returns rows for
+ // an IN/= filter that the engine's own (case-sensitive) key comparison would
+ // not consider equal. Used by the known-issue test for ticket #502.
+ bool CaseInsensitiveFilter;
+
+ int ColumnIndex(const std::wstring& name) const
+ {
+ for (size_t i = 0; i < Columns.size(); ++i)
+ {
+ if (Columns[i].Name == name)
+ return static_cast<int>(i);
+ }
+ return -1;
+ }
+ };
+
+ static const FakeValue* FindValue(const FakeTable& table, const std::vector<FakeValue>& row,
+ FdoString* name)
+ {
+ if (name == NULL)
+ return NULL;
+
+ int index = table.ColumnIndex(name);
+ if (index < 0 || index >= static_cast<int>(row.size()))
+ return NULL;
+
+ return &row[index];
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Filter evaluation
+ //
+ // The nested loop joins re-execute the right-hand query with an equality
+ // filter, and the batch join re-executes it with an IN filter. Evaluating
+ // those against the in-memory rows is what makes the mocks behave like a
+ // provider that pushed the filter down.
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ static std::wstring ValueText(FdoDataValue* value)
+ {
+ FdoStringValue* stringValue = dynamic_cast<FdoStringValue*>(value);
+ if (stringValue != NULL)
+ return stringValue->GetString();
+ return value->ToString();
+ }
+
+ static bool LikeMatch(const std::wstring& text, const std::wstring& pattern)
+ {
+ // Small wildcard matcher supporting the % and _ wildcards.
+ size_t ti = 0, pi = 0;
+ size_t star = std::wstring::npos, mark = 0;
+
+ while (ti < text.size())
+ {
+ if (pi < pattern.size() && (pattern[pi] == L'_' || pattern[pi] == text[ti]))
+ {
+ ++ti;
+ ++pi;
+ }
+ else if (pi < pattern.size() && pattern[pi] == L'%')
+ {
+ star = pi++;
+ mark = ti;
+ }
+ else if (star != std::wstring::npos)
+ {
+ pi = star + 1;
+ ti = ++mark;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ while (pi < pattern.size() && pattern[pi] == L'%')
+ ++pi;
+
+ return pi == pattern.size();
+ }
+
+ static bool EvaluateFilter(const FakeTable& table, const std::vector<FakeValue>& row, FdoFilter* filter);
+
+ static bool EvaluateComparison(const FakeValue& value, FdoDataValue* right,
+ FdoComparisonOperations operation, bool caseInsensitive)
+ {
+ FdoPtr<FdoDataValue> left = value.CreateDataValue();
+
+ if (operation == FdoComparisonOperations_Like)
+ return LikeMatch(value.AsText(), ValueText(right));
+
+ if (operation != FdoComparisonOperations_EqualTo)
+ return true; // Unsupported operator: behave like a provider that cannot push it down
+
+ // A provider never reports NULL = NULL as a match, so null keys do not join.
+ if (left->IsNull() || right->IsNull())
+ return false;
+
+ FdoStringValue* rightString = dynamic_cast<FdoStringValue*>(right);
+ if (caseInsensitive && value.Type == FdoDataType_String && rightString != NULL)
+ return _wcsicmp(value.StringValue.c_str(), rightString->GetString()) == 0;
+
+ return GWSFdoUtilities::CompareDataValues(left, right) == 0;
+ }
+
+ static bool EvaluateFilter(const FakeTable& table, const std::vector<FakeValue>& row, FdoFilter* filter)
+ {
+ if (filter == NULL)
+ return true;
+
+ FdoBinaryLogicalOperator* binary = dynamic_cast<FdoBinaryLogicalOperator*>(filter);
+ if (binary != NULL)
+ {
+ FdoPtr<FdoFilter> leftOperand = binary->GetLeftOperand();
+ FdoPtr<FdoFilter> rightOperand = binary->GetRightOperand();
+ bool left = EvaluateFilter(table, row, leftOperand);
+ bool right = EvaluateFilter(table, row, rightOperand);
+ return binary->GetOperation() == FdoBinaryLogicalOperations_And ? (left && right) : (left || right);
+ }
+
+ FdoComparisonCondition* comparison = dynamic_cast<FdoComparisonCondition*>(filter);
+ if (comparison != NULL)
+ {
+ FdoPtr<FdoExpression> leftExpression = comparison->GetLeftExpression();
+ FdoPtr<FdoExpression> rightExpression = comparison->GetRightExpression();
+
+ FdoIdentifier* identifier = dynamic_cast<FdoIdentifier*>(leftExpression.p);
+ FdoDataValue* rightValue = dynamic_cast<FdoDataValue*>(rightExpression.p);
+ if (identifier == NULL || rightValue == NULL)
+ return true;
+
+ const FakeValue* value = FindValue(table, row, identifier->GetName());
+ if (value == NULL)
+ return false;
+
+ return EvaluateComparison(*value, rightValue, comparison->GetOperation(),
+ table.CaseInsensitiveFilter);
+ }
+
+ FdoInCondition* inCondition = dynamic_cast<FdoInCondition*>(filter);
+ if (inCondition != NULL)
+ {
+ FdoPtr<FdoIdentifier> identifier = inCondition->GetPropertyName();
+ FdoPtr<FdoValueExpressionCollection> values = inCondition->GetValues();
+ if (identifier == NULL || values == NULL)
+ return true;
+
+ const FakeValue* value = FindValue(table, row, identifier->GetName());
+ if (value == NULL)
+ return false;
+
+ FdoPtr<FdoDataValue> left = value->CreateDataValue();
+ for (FdoInt32 i = 0; i < values->GetCount(); ++i)
+ {
+ FdoPtr<FdoValueExpression> candidate = values->GetItem(i);
+ FdoDataValue* right = dynamic_cast<FdoDataValue*>(candidate.p);
+ if (right == NULL || left->IsNull() || right->IsNull())
+ continue;
+
+ FdoStringValue* leftString = dynamic_cast<FdoStringValue*>(left.p);
+ FdoStringValue* rightString = dynamic_cast<FdoStringValue*>(right);
+ if (table.CaseInsensitiveFilter && leftString != NULL && rightString != NULL)
+ {
+ if (_wcsicmp(leftString->GetString(), rightString->GetString()) == 0)
+ return true;
+ }
+ else if (GWSFdoUtilities::CompareDataValues(left, right) == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Result descriptor / class definition
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ static FdoPtr<CGwsQueryResultDescriptors> BuildDescriptor(const FakeTable& table)
+ {
+ FdoPtr<FdoFeatureClass> classDef = FdoFeatureClass::Create();
+ classDef->SetName(table.ClassName.c_str());
+
+ FdoPtr<FdoPropertyDefinitionCollection> properties = classDef->GetProperties();
+ FdoPtr<FdoDataPropertyDefinitionCollection> identity = classDef->GetIdentityProperties();
+ FdoPtr<FdoIdentifierCollection> selectList = FdoIdentifierCollection::Create();
+
+ for (size_t i = 0; i < table.Columns.size(); ++i)
+ {
+ const FakeColumn& column = table.Columns[i];
+
+ FdoPtr<FdoDataPropertyDefinition> property = FdoDataPropertyDefinition::Create();
+ property->SetName(column.Name.c_str());
+ property->SetDataType(column.Type);
+ property->SetNullable(true);
+ if (column.Type == FdoDataType_String)
+ property->SetLength(255);
+ properties->Add(property);
+
+ if (column.IsIdentity)
+ identity->Add(property);
+
+ FdoPtr<FdoIdentifier> identifier = FdoIdentifier::Create(column.Name.c_str());
+ selectList->Add(identifier);
+ }
+
+ GWSQualifiedName className(L"TestSchema", table.ClassName.c_str());
+ CGwsQueryResultDescriptors* descriptor = new CGwsQueryResultDescriptors(
+ classDef, className, NULL, NULL, true, selectList, false);
+ descriptor->AddRef();
+ return FdoPtr<CGwsQueryResultDescriptors>(descriptor);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Mock feature iterator
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ class FakeFeatureIterator : public CGwsFeatureIterator
+ {
+ public:
+ FakeFeatureIterator(CGwsQueryResultDescriptors* descriptor, const FakeTable* table,
+ FdoFilter* filter)
+ : m_descriptor(descriptor), m_table(table), m_filter(NULL), m_position(-1)
+ {
+ m_descriptor->AddRef();
+ if (filter != NULL)
+ {
+ filter->AddRef();
+ m_filter = filter;
+ }
+
+ for (size_t i = 0; i < m_table->Rows.size(); ++i)
+ {
+ if (EvaluateFilter(*m_table, m_table->Rows[i], m_filter))
+ m_rows.push_back(static_cast<int>(i));
+ }
+ }
+
+ virtual ~FakeFeatureIterator()
+ {
+ if (m_filter != NULL)
+ m_filter->Release();
+ m_descriptor->Release();
+ }
+
+ virtual bool ReadNext()
+ {
+ ++m_position;
+ return m_position < static_cast<int>(m_rows.size());
+ }
+
+ virtual void Close()
+ {
+ m_position = static_cast<int>(m_rows.size());
+ }
+
+ virtual void DescribeFeature(IGWSExtendedFeatureDescription** ppResultDesc)
+ {
+ if (ppResultDesc == NULL)
+ return;
+ m_descriptor->AddRef();
+ *ppResultDesc = m_descriptor;
+ }
+
+ virtual FdoDataValue* GetDataValue(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ if (value == NULL)
+ return NULL;
+ return value->CreateDataValue();
+ }
+
+ virtual FdoDataValueCollection* GetDataValues(FdoStringCollection* propertyNames)
+ {
+ FdoPtr<FdoDataValueCollection> values = CGwsDataValueCollection::Create();
+ for (FdoInt32 i = 0; propertyNames != NULL && i < propertyNames->GetCount(); ++i)
+ {
+ FdoPtr<FdoDataValue> value = GetDataValue(propertyNames->GetString(i));
+ values->Add(value);
+ }
+ return values.Detach();
+ }
+
+ virtual bool IsNull(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ return value == NULL || value->IsNullValue;
+ }
+
+ virtual FdoString* GetString(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ if (value == NULL || value->IsNullValue)
+ return NULL;
+ return const_cast<FdoString*>(value->StringValue.c_str());
+ }
+
+ virtual bool GetBoolean(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ return value != NULL && !value->IsNullValue && value->IntValue != 0;
+ }
+
+ virtual FdoByte GetByte(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ return value == NULL ? 0 : static_cast<FdoByte>(value->IntValue);
+ }
+
+ virtual FdoDateTime GetDateTime(FdoString* propertyName)
+ {
+ return FdoDateTime();
+ }
+
+ virtual double GetDouble(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ if (value == NULL)
+ return 0.0;
+ return value->Type == FdoDataType_Double ? value->DoubleValue
+ : static_cast<double>(value->IntValue);
+ }
+
+ virtual FdoInt16 GetInt16(FdoString* propertyName)
+ {
+ return static_cast<FdoInt16>(GetInt64(propertyName));
+ }
+
+ virtual FdoInt32 GetInt32(FdoString* propertyName)
+ {
+ return static_cast<FdoInt32>(GetInt64(propertyName));
+ }
+
+ virtual FdoInt64 GetInt64(FdoString* propertyName)
+ {
+ const FakeValue* value = CurrentValue(propertyName);
+ if (value == NULL)
+ return 0;
+ return value->Type == FdoDataType_Double ? static_cast<FdoInt64>(value->DoubleValue)
+ : value->IntValue;
+ }
+
+ virtual float GetSingle(FdoString* propertyName)
+ {
+ return static_cast<float>(GetDouble(propertyName));
+ }
+
+ virtual FdoInt32 GetRevisionNumber()
+ {
+ return -1;
+ }
+
+ virtual FdoInt32 GetCacheId()
+ {
+ return 0;
+ }
+
+ virtual GWSFeatureId GetFeatureId()
+ {
+ FdoPtr<FdoDataValueCollection> values = CGwsDataValueCollection::Create();
+ for (size_t i = 0; i < m_table->Columns.size(); ++i)
+ {
+ if (m_table->Columns[i].IsIdentity)
+ {
+ FdoPtr<FdoDataValue> value = GetDataValue(m_table->Columns[i].Name.c_str());
+ values->Add(value);
+ }
+ }
+ return GWSFeatureId(values);
+ }
+
+ virtual FdoClassDefinition* GetClassDefinition()
+ {
+ return m_descriptor->ClassDefinition();
+ }
+
+ virtual FdoString* GetPropertyName(FdoInt32 index)
+ {
+ if (index < 0 || index >= static_cast<int>(m_table->Columns.size()))
+ return NULL;
+ return const_cast<FdoString*>(m_table->Columns[index].Name.c_str());
+ }
+
+ virtual FdoInt32 GetPropertyIndex(FdoString* propertyName)
+ {
+ return m_table->ColumnIndex(propertyName == NULL ? L"" : propertyName);
+ }
+
+ private:
+ const FakeValue* CurrentValue(FdoString* propertyName) const
+ {
+ if (m_position < 0 || m_position >= static_cast<int>(m_rows.size()))
+ return NULL;
+ return FindValue(*m_table, m_table->Rows[m_rows[m_position]], propertyName);
+ }
+
+ CGwsQueryResultDescriptors* m_descriptor;
+ const FakeTable* m_table;
+ FdoFilter* m_filter;
+ std::vector<int> m_rows;
+ int m_position;
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Mock prepared query: the "source" the join algorithms execute.
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ class FakePreparedQuery : public CGwsPreparedQuery
+ {
+ public:
+ FakePreparedQuery(CGwsQueryResultDescriptors* descriptor, const FakeTable* table)
+ : m_descriptor(descriptor), m_table(table), m_executeCount(0)
+ {
+ m_descriptor->AddRef();
+ m_resultDescriptor = m_descriptor;
+ m_pathlength = 0;
+ }
+
+ virtual ~FakePreparedQuery()
+ {
+ }
+
+ int ExecuteCount() const
+ {
+ return m_executeCount;
+ }
+
+ virtual EGwsQueryType QueryType() const
+ {
+ return eGwsQueryFeature;
+ }
+
+ virtual EGwsStatus Execute(IGWSFeatureIterator** results, bool bScrollable = false)
+ {
+ if (results == NULL)
+ return eGwsFailed;
+
+ ++m_executeCount;
+ *results = NULL;
+ FakeFeatureIterator* iterator = new FakeFeatureIterator(m_descriptor, m_table, m_filter);
+ iterator->AddRef();
+ *results = iterator;
+ return eGwsOk;
+ }
+
+ virtual EGwsStatus Execute(FdoFilter* filter, IGWSFeatureIterator** results,
+ bool bScrollable = false)
+ {
+ SetFilter(filter);
+ return Execute(results, bScrollable);
+ }
+
+ virtual EGwsStatus Execute(const GWSFeatureId& featureId, IGWSFeatureIterator** results)
+ {
+ return Execute(results);
+ }
+
+ virtual EGwsStatus Execute(const GwsFeaturesIdVector& featureIds, int lowerBound,
+ int upperBound, IGWSFeatureIterator** results,
+ bool bScrollable = false)
+ {
+ return Execute(results, bScrollable);
+ }
+
+ virtual EGwsStatus SetFilter(FdoFilter* filter)
+ {
+ if (filter != NULL)
+ filter->AddRef();
+ m_filter = filter;
+ return eGwsOk;
+ }
+
+ virtual FdoFilter* GetFilter()
+ {
+ if (m_filter != NULL)
+ m_filter.p->AddRef();
+ return m_filter;
+ }
+
+ virtual CGwsPreparedFeatureQuery* GetPrimaryQuery()
+ {
+ return NULL;
+ }
+
+ virtual FdoDataPropertyDefinitionCollection* GetIdentityProperties()
+ {
+ return m_descriptor->GetIdentityProperties();
+ }
+
+ private:
+ CGwsQueryResultDescriptors* m_descriptor;
+ const FakeTable* m_table;
+ FdoPtr<FdoFilter> m_filter;
+ int m_executeCount;
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Mock prepared join query
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ class FakeLeftJoinQuery : public CGwsPreparedLeftJoinQuery
+ {
+ public:
+ FakeLeftJoinQuery(EGwsJoinMethod method, FakePreparedQuery* left, FakePreparedQuery* right,
+ FdoStringCollection* leftColumns, FdoStringCollection* rightColumns,
+ CGwsFeatureSourceQuery* query)
+ : CGwsPreparedLeftJoinQuery(method, left, right, leftColumns, rightColumns, query)
+ {
+ }
+ };
+
+ class FakeEqualJoinQuery : public CGwsPreparedEqualJoinQuery
+ {
+ public:
+ FakeEqualJoinQuery(EGwsJoinMethod method, FakePreparedQuery* left, FakePreparedQuery* right,
+ FdoStringCollection* leftColumns, FdoStringCollection* rightColumns,
+ CGwsFeatureSourceQuery* query)
+ : CGwsPreparedEqualJoinQuery(method, left, right, leftColumns, rightColumns, query)
+ {
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Join driver
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ static std::wstring ReadString(IGWSFeatureIterator* iterator, const wchar_t* property)
+ {
+ if (iterator == NULL || iterator->IsNull(property))
+ return std::wstring();
+ FdoString* value = iterator->GetString(property);
+ return value == NULL ? std::wstring() : std::wstring(value);
+ }
+
+ // Runs one join and returns its rows as "<left name>=<right label>" strings.
+ // A left row with no matching right row is reported as "<left name>=NULL".
+ static std::vector<std::wstring> RunJoin(const FakeTable& leftTable, const FakeTable& rightTable,
+ const std::vector<std::wstring>& leftJoinProperties,
+ const std::vector<std::wstring>& rightJoinProperties,
+ EGwsJoinMethod method, EGwsQueryType joinType,
+ bool forceOneToOne = true,
+ int* rightExecuteCount = NULL)
+ {
+ FdoPtr<CGwsQueryResultDescriptors> leftDescriptor = BuildDescriptor(leftTable);
+ FdoPtr<CGwsQueryResultDescriptors> rightDescriptor = BuildDescriptor(rightTable);
+
+ // The join prepared query takes ownership of the two prepared queries.
+ FakePreparedQuery* leftQuery = new FakePreparedQuery(leftDescriptor, &leftTable);
+ FakePreparedQuery* rightQuery = new FakePreparedQuery(rightDescriptor, &rightTable);
+
+ FdoPtr<FdoStringCollection> leftColumns = FdoStringCollection::Create();
+ for (size_t i = 0; i < leftJoinProperties.size(); ++i)
+ leftColumns->Add(leftJoinProperties[i].c_str());
+ FdoPtr<FdoStringCollection> rightColumns = FdoStringCollection::Create();
+ for (size_t i = 0; i < rightJoinProperties.size(); ++i)
+ rightColumns->Add(rightJoinProperties[i].c_str());
+
+ FdoPtr<IGWSFeatureQueryDefinition> leftDefinition = IGWSFeatureQueryDefinition::Create(
+ NULL, GWSQualifiedName(L"TestSchema", leftTable.ClassName.c_str()), NULL);
+ FdoPtr<IGWSFeatureQueryDefinition> rightDefinition = IGWSFeatureQueryDefinition::Create(
+ NULL, GWSQualifiedName(L"TestSchema", rightTable.ClassName.c_str()), NULL);
+
+ FdoPtr<IGWSQueryDefinition> joinDefinition;
+ if (joinType == eGwsQueryLeftOuterJoin)
+ {
+ joinDefinition = IGWSLeftJoinQueryDefinition::Create(
+ L"Join1", L"|", forceOneToOne, leftDefinition, rightDefinition, leftColumns, rightColumns);
+ }
+ else
+ {
+ joinDefinition = IGWSEqualJoinQueryDefinition::Create(
+ L"Join1", L"|", forceOneToOne, leftDefinition, rightDefinition, leftColumns, rightColumns);
+ }
+
+ FdoPtr<CGwsFeatureSourceQuery> sourceQuery =
+ static_cast<CGwsFeatureSourceQuery*>(
+ CGwsFeatureSourceQuery::CreateInstance<CGwsFeatureSourceQuery>(NULL));
+ sourceQuery->Initialize(NULL, joinDefinition);
+
+ CGwsPreparedJoinQuery* joinQuery = NULL;
+ if (joinType == eGwsQueryLeftOuterJoin)
+ {
+ joinQuery = new FakeLeftJoinQuery(method, leftQuery, rightQuery, leftColumns,
+ rightColumns, sourceQuery.p);
+ }
+ else
+ {
+ joinQuery = new FakeEqualJoinQuery(method, leftQuery, rightQuery, leftColumns,
+ rightColumns, sourceQuery.p);
+ }
+
+ EGwsStatus status = joinQuery->Init();
+ REQUIRE(status == eGwsOk);
+
+ std::vector<std::wstring> rows;
+
+ {
+ FdoPtr<IGWSFeatureIterator> iterator;
+ status = joinQuery->Execute(&iterator, false);
+ REQUIRE(status == eGwsOk);
+ REQUIRE(iterator.p != NULL);
+
+ while (iterator->ReadNext())
+ {
+ // Resolve the joined features before reading the primary values: the
+ // batch join populates its primary-row cache while resolving the
+ // right-hand side.
+ FdoPtr<IGWSFeatureIterator> rightIterator = iterator->GetJoinedFeatures(0);
+ std::wstring leftName = ReadString(iterator, L"name");
+
+ // A left-outer join with no matching right row is reported by the
+ // batch join as a right iterator positioned on a "null entry"
+ // (every property reads as null) rather than by ReadNext() returning
+ // false, so treat that as "no match" too.
+ bool hasRight = rightIterator.p != NULL
+ && rightIterator->ReadNext()
+ && !rightIterator->IsNull(L"label");
+
+ if (!hasRight)
+ {
+ if (joinType == eGwsQueryLeftOuterJoin)
+ rows.push_back(leftName + L"=NULL");
+ continue;
+ }
+
+ do
+ {
+ rows.push_back(leftName + L"=" + ReadString(rightIterator, L"label"));
+ } while (rightIterator->ReadNext() && !rightIterator->IsNull(L"label"));
+ }
+ }
+
+ if (rightExecuteCount != NULL)
+ *rightExecuteCount = rightQuery->ExecuteCount();
+
+ delete joinQuery;
+ return rows;
+ }
+
+ static std::vector<std::wstring> RunJoin(const FakeTable& leftTable, const FakeTable& rightTable,
+ const wchar_t* leftJoinProperty,
+ const wchar_t* rightJoinProperty,
+ EGwsJoinMethod method, EGwsQueryType joinType,
+ bool forceOneToOne = true)
+ {
+ std::vector<std::wstring> leftProperties(1, leftJoinProperty);
+ std::vector<std::wstring> rightProperties(1, rightJoinProperty);
+ return RunJoin(leftTable, rightTable, leftProperties, rightProperties,
+ method, joinType, forceOneToOne);
+ }
+
+ static const wchar_t* JoinMethodName(EGwsJoinMethod method)
+ {
+ switch (method)
+ {
+ case eGwsSortMerge:
+ return L"eGwsSortMerge";
+ case eGwsNestedLoops:
+ return L"eGwsNestedLoops";
+ case eGwsNestedLoopSortedBlock:
+ return L"eGwsNestedLoopSortedBlock";
+ case eGwsBatchSortedBlock:
+ return L"eGwsBatchSortedBlock";
+ case eGwsHash:
+ return L"eGwsHash";
+ default:
+ return L"<unknown>";
+ }
+ }
+
+ static const EGwsJoinMethod kImplementedJoinMethods[] =
+ {
+ eGwsSortMerge,
+ eGwsNestedLoops,
+ eGwsNestedLoopSortedBlock,
+ eGwsBatchSortedBlock
+ };
+
+ static std::string Describe(const std::vector<std::wstring>& rows)
+ {
+ std::string text;
+ for (size_t i = 0; i < rows.size(); ++i)
+ {
+ if (i > 0)
+ text += ";";
+ text += Narrow(rows[i]);
+ }
+ return text;
+ }
+
+ // Same as Describe() but insensitive to the order the algorithm emits rows in.
+ static std::string DescribeSorted(std::vector<std::wstring> rows)
+ {
+ std::sort(rows.begin(), rows.end());
+ return Describe(rows);
+ }
+
+ static std::string MethodAndTypeName(EGwsJoinMethod method, EGwsQueryType joinType)
+ {
+ std::string name = Narrow(JoinMethodName(method));
+ name += joinType == eGwsQueryLeftOuterJoin ? " (left outer)" : " (inner)";
+ return name;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ //
+ // Table fixtures
+ //
+ ///////////////////////////////////////////////////////////////////////////
+
+ static FakeTable MakeIntTable(const wchar_t* className,
+ const std::vector<std::pair<int, std::wstring> >& rows)
+ {
+ FakeTable table;
+ table.ClassName = className;
+ table.Columns.push_back(FakeColumn(L"id", FdoDataType_Int32, true));
+ table.Columns.push_back(FakeColumn(L"name", FdoDataType_String, false));
+
+ for (size_t i = 0; i < rows.size(); ++i)
+ {
+ std::vector<FakeValue> row;
+ row.push_back(FakeValue::Int32(rows[i].first));
+ row.push_back(FakeValue::String(rows[i].second.c_str()));
+ table.Rows.push_back(row);
+ }
+
+ return table;
+ }
+
+ static FakeTable MakeRightIntTable(const std::vector<std::pair<int, std::wstring> >& rows)
+ {
+ FakeTable table = MakeIntTable(L"RightClass", rows);
+ table.Columns[1].Name = L"label";
+ for (size_t i = 0; i < table.Rows.size(); ++i)
+ {
+ table.Rows[i][1].StringValue = rows[i].second;
+ }
+ return table;
+ }
+
+ static FakeTable MakeTable(const wchar_t* className, const std::vector<FakeColumn>& columns,
+ const std::vector<std::vector<FakeValue> >& rows)
+ {
+ FakeTable table;
+ table.ClassName = className;
+ table.Columns = columns;
+ table.Rows = rows;
+ return table;
+ }
+
+ static FakeTable MakeStringTable(const wchar_t* className,
+ const std::vector<std::pair<std::wstring, std::wstring> >& rows)
+ {
+ FakeTable table;
+ table.ClassName = className;
+ table.Columns.push_back(FakeColumn(L"code", FdoDataType_String, true));
+ table.Columns.push_back(FakeColumn(L"name", FdoDataType_String, false));
+
+ for (size_t i = 0; i < rows.size(); ++i)
+ {
+ std::vector<FakeValue> row;
+ row.push_back(FakeValue::String(rows[i].first.c_str()));
+ row.push_back(FakeValue::String(rows[i].second.c_str()));
+ table.Rows.push_back(row);
+ }
+
+ return table;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Join algorithms
+// ---------------------------------------------------------------------------
+
+TEST_CASE("GwsJoinOneToOne", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass", { { 1, L"L1" }, { 2, L"L2" }, { 3, L"L3" } });
+ FakeTable right = MakeRightIntTable({ { 2, L"R2" }, { 3, L"R3" }, { 4, L"R4" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L2=R2;L3=R3");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=NULL;L2=R2;L3=R3");
+ }
+}
+
+TEST_CASE("GwsJoinOneToMany", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass", { { 1, L"L1" }, { 2, L"L2" }, { 3, L"L3" } });
+ FakeTable right = MakeRightIntTable(
+ { { 1, L"R1" }, { 2, L"R2a" }, { 2, L"R2b" }, { 3, L"R3a" }, { 3, L"R3b" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L2=R2a;L2=R2b;L3=R3a;L3=R3b");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=R2a;L2=R2b;L3=R3a;L3=R3b");
+ }
+}
+
+TEST_CASE("GwsJoinManyToOne", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 1, L"L1b" }, { 2, L"L2" }, { 3, L"L3" } });
+ FakeTable right = MakeRightIntTable({ { 1, L"R1" }, { 2, L"R2" }, { 3, L"R3" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L1b=R1;L2=R2;L3=R3");
+ }
+}
+
+TEST_CASE("GwsJoinWithGapsOnBothSides", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 2, L"L2" }, { 3, L"L3" }, { 6, L"L6" } });
+ FakeTable right = MakeRightIntTable(
+ { { 1, L"R1" }, { 3, L"R3a" }, { 3, L"R3b" }, { 5, L"R5" }, { 6, L"R6" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L3=R3a;L3=R3b;L6=R6");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=NULL;L3=R3a;L3=R3b;L6=R6");
+ }
+}
+
+TEST_CASE("GwsJoinStringKeys", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeStringTable(L"LeftClass",
+ { { L"alpha", L"LA" }, { L"bravo", L"LB" }, { L"charlie", L"LC" } });
+
+ FakeTable right;
+ right.ClassName = L"RightClass";
+ right.Columns.push_back(FakeColumn(L"code", FdoDataType_String, true));
+ right.Columns.push_back(FakeColumn(L"label", FdoDataType_String, false));
+ right.Rows.push_back({ FakeValue::String(L"alpha"), FakeValue::String(L"RA") });
+ right.Rows.push_back({ FakeValue::String(L"bravo"), FakeValue::String(L"RBa") });
+ right.Rows.push_back({ FakeValue::String(L"bravo"), FakeValue::String(L"RBb") });
+ right.Rows.push_back({ FakeValue::String(L"zulu"), FakeValue::String(L"RZ") });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"code", L"code", method, eGwsQueryEqualJoin))
+ == "LA=RA;LB=RBa;LB=RBb");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"code", L"code", method, eGwsQueryLeftOuterJoin))
+ == "LA=RA;LB=RBa;LB=RBb;LC=NULL");
+ }
+}
+
+TEST_CASE("GwsJoinMultiColumn", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeTable(L"LeftClass",
+ { FakeColumn(L"id", FdoDataType_Int32, true),
+ FakeColumn(L"code", FdoDataType_String, false),
+ FakeColumn(L"name", FdoDataType_String, false) },
+ {
+ { FakeValue::Int32(1), FakeValue::String(L"a"), FakeValue::String(L"L1") },
+ { FakeValue::Int32(1), FakeValue::String(L"b"), FakeValue::String(L"L2") },
+ { FakeValue::Int32(2), FakeValue::String(L"a"), FakeValue::String(L"L3") }
+ });
+
+ FakeTable right = MakeTable(L"RightClass",
+ { FakeColumn(L"id", FdoDataType_Int32, true),
+ FakeColumn(L"code", FdoDataType_String, false),
+ FakeColumn(L"label", FdoDataType_String, false) },
+ {
+ { FakeValue::Int32(1), FakeValue::String(L"b"), FakeValue::String(L"R1b") },
+ { FakeValue::Int32(2), FakeValue::String(L"a"), FakeValue::String(L"R2a") },
+ { FakeValue::Int32(2), FakeValue::String(L"b"), FakeValue::String(L"R2b") }
+ });
+
+ std::vector<std::wstring> joinProperties;
+ joinProperties.push_back(L"id");
+ joinProperties.push_back(L"code");
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ // The batch join only supports a single join column.
+ if (method == eGwsBatchSortedBlock)
+ continue;
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, joinProperties, joinProperties, method, eGwsQueryEqualJoin))
+ == "L2=R1b;L3=R2a");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, joinProperties, joinProperties, method, eGwsQueryLeftOuterJoin))
+ == "L1=NULL;L2=R1b;L3=R2a");
+ }
+}
+
+TEST_CASE("GwsJoinNullKeys", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeTable(L"LeftClass",
+ { FakeColumn(L"id", FdoDataType_Int32, true),
+ FakeColumn(L"name", FdoDataType_String, false) },
+ {
+ { FakeValue::Int32(1), FakeValue::String(L"L1") },
+ { FakeValue::Int32(3), FakeValue::String(L"L3") },
+ { FakeValue::Null(FdoDataType_Int32), FakeValue::String(L"LN") }
+ });
+
+ FakeTable right = MakeTable(L"RightClass",
+ { FakeColumn(L"id", FdoDataType_Int32, true),
+ FakeColumn(L"label", FdoDataType_String, false) },
+ {
+ { FakeValue::Int32(1), FakeValue::String(L"R1") },
+ { FakeValue::Int32(3), FakeValue::String(L"R3") },
+ { FakeValue::Null(FdoDataType_Int32), FakeValue::String(L"RN") }
+ });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ // A NULL join key never matches another NULL join key.
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L3=R3");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L3=R3;LN=NULL");
+ }
+}
+
+TEST_CASE("GwsJoinEmptySources", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass", { { 1, L"L1" }, { 2, L"L2" } });
+ FakeTable emptyRight = MakeRightIntTable({});
+ FakeTable emptyLeft = MakeIntTable(L"LeftClass", {});
+ FakeTable right = MakeRightIntTable({ { 1, L"R1" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, emptyRight, L"id", L"id", method, eGwsQueryEqualJoin)).empty());
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, emptyRight, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=NULL;L2=NULL");
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(emptyLeft, right, L"id", L"id", method, eGwsQueryEqualJoin)).empty());
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(emptyLeft, right, L"id", L"id", method, eGwsQueryLeftOuterJoin)).empty());
+ }
+}
+
+TEST_CASE("GwsJoinForceOneToOne", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass", { { 1, L"L1" }, { 2, L"L2" } });
+ FakeTable right = MakeRightIntTable({ { 2, L"R2a" }, { 2, L"R2b" } });
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin, true))
+ == "L2=R2a;L2=R2b");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin, true))
+ == "L1=NULL;L2=R2a;L2=R2b");
+ }
+}
+
+TEST_CASE("GwsJoinBatchBlocks", "[GwsQueryEngine]")
+{
+ // Force the batch sorted block join to span more than one batch.
+ int previousBatchSize = CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize;
+ CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize = 2;
+
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 2, L"L2" }, { 3, L"L3" }, { 4, L"L4" }, { 5, L"L5" } });
+ FakeTable right = MakeRightIntTable(
+ { { 1, L"R1" }, { 2, L"R2" }, { 4, L"R4" }, { 5, L"R5a" }, { 5, L"R5b" } });
+
+ EGwsJoinMethod method = eGwsBatchSortedBlock;
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L2=R2;L4=R4;L5=R5a;L5=R5b");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=R2;L3=NULL;L4=R4;L5=R5a;L5=R5b");
+
+ CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize = previousBatchSize;
+}
+
+TEST_CASE("GwsJoinManyRows", "[GwsQueryEngine]")
+{
+ // Enough rows to exercise the incremental cursor bookkeeping in every algorithm.
+ std::vector<std::pair<int, std::wstring> > leftRows;
+ std::vector<std::pair<int, std::wstring> > rightRows;
+ for (int i = 0; i < 40; ++i)
+ {
+ leftRows.push_back(std::make_pair(i, L"L" + std::to_wstring(i)));
+ if (i % 3 != 0)
+ rightRows.push_back(std::make_pair(i, L"R" + std::to_wstring(i)));
+ }
+
+ FakeTable left = MakeIntTable(L"LeftClass", leftRows);
+ FakeTable right = MakeRightIntTable(rightRows);
+
+ std::wstring expectedOuter;
+ std::wstring expectedInner;
+ for (int i = 0; i < 40; ++i)
+ {
+ std::wstring leftName = L"L" + std::to_wstring(i);
+ if (i % 3 != 0)
+ {
+ std::wstring row = leftName + L"=R" + std::to_wstring(i);
+ expectedInner += (expectedInner.empty() ? L"" : L";") + row;
+ expectedOuter += (expectedOuter.empty() ? L"" : L";") + row;
+ }
+ else
+ {
+ expectedOuter += (expectedOuter.empty() ? L"" : L";") + leftName + L"=NULL";
+ }
+ }
+
+ for (size_t i = 0; i < sizeof(kImplementedJoinMethods) / sizeof(kImplementedJoinMethods[0]); ++i)
+ {
+ EGwsJoinMethod method = kImplementedJoinMethods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == Narrow(expectedInner));
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == Narrow(expectedOuter));
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Batch sorted block
+//
+// eGwsBatchSortedBlock is chosen when the primary (left) source cannot order its
+// results but the secondary can, which is exactly the SDF/SHP-joined-to-a-RDBMS
+// case behind tickets #502, #791, #1396 and #1790. #1888 (r6333/r6348) made the
+// engine stop choosing it for SDF/SHP rather than fix it, so these tests cover
+// the algorithm directly.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+TEST_CASE("GwsJoinUnsortedPrimary", "[GwsQueryEngine]")
+{
+ // The batch join is selected when the primary source cannot order its rows, so
+ // its input is naturally unsorted. Sort-merge is excluded: it is only selected
+ // when the primary query supplies the ordering.
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 3, L"L3" }, { 1, L"L1" }, { 5, L"L5" }, { 2, L"L2" }, { 4, L"L4" } });
+ FakeTable right = MakeRightIntTable(
+ { { 1, L"R1" }, { 2, L"R2" }, { 4, L"R4" }, { 5, L"R5a" }, { 5, L"R5b" } });
+
+ const EGwsJoinMethod methods[] =
+ {
+ eGwsNestedLoops,
+ eGwsNestedLoopSortedBlock,
+ eGwsBatchSortedBlock
+ };
+
+ for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); ++i)
+ {
+ EGwsJoinMethod method = methods[i];
+
+ INFO(MethodAndTypeName(method, eGwsQueryEqualJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", method, eGwsQueryEqualJoin))
+ == "L1=R1;L2=R2;L4=R4;L5=R5a;L5=R5b");
+
+ INFO(MethodAndTypeName(method, eGwsQueryLeftOuterJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", method, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=R2;L3=NULL;L4=R4;L5=R5a;L5=R5b");
+ }
+}
+
+TEST_CASE("GwsBatchSortedBlockIsIncremental", "[GwsQueryEngine]")
+{
+ // #791 was a join performance problem. The batch algorithm exists to execute the
+ // secondary query once per block of primary rows (via an IN clause), not once per
+ // primary row; this asserts that shape without measuring time.
+ int previousBatchSize = CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize;
+ CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize = 3;
+
+ std::vector<std::pair<int, std::wstring> > leftRows;
+ std::vector<std::pair<int, std::wstring> > rightRows;
+ for (int i = 1; i <= 10; ++i)
+ {
+ leftRows.push_back(std::make_pair(i, L"L" + std::to_wstring(i)));
+ rightRows.push_back(std::make_pair(i, L"R" + std::to_wstring(i)));
+ }
+
+ FakeTable left = MakeIntTable(L"LeftClass", leftRows);
+ FakeTable right = MakeRightIntTable(rightRows);
+
+ std::vector<std::wstring> joinProperties(1, L"id");
+
+ int batchExecutions = 0;
+ RunJoin(left, right, joinProperties, joinProperties,
+ eGwsBatchSortedBlock, eGwsQueryEqualJoin, true, &batchExecutions);
+
+ int nestedLoopExecutions = 0;
+ RunJoin(left, right, joinProperties, joinProperties,
+ eGwsNestedLoops, eGwsQueryEqualJoin, true, &nestedLoopExecutions);
+
+ INFO("batch=" << batchExecutions << " nested=" << nestedLoopExecutions);
+ CHECK(batchExecutions <= 4); // 10 primary rows / 3 per batch
+ CHECK(nestedLoopExecutions >= 10); // at least one query per primary row
+
+ CGwsBatchSortedBlockJoinQueryResults::sm_nBatchSize = previousBatchSize;
+}
+
+// Duplicate primary join keys (ticket #1396, and the "similar join property
+// values" of #1790). The batch materialises and sorts the secondary rows for the
+// block, so each primary row - including every member of a duplicate group -
+// looks up its own matches instead of walking a single secondary cursor.
+TEST_CASE("GwsBatchSortedBlockDuplicatePrimaryKeys", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 1, L"L1b" }, { 2, L"L2" }, { 3, L"L3" } });
+ FakeTable right = MakeRightIntTable({ { 1, L"R1" }, { 2, L"R2" }, { 3, L"R3" } });
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryEqualJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", eGwsBatchSortedBlock, eGwsQueryEqualJoin))
+ == "L1=R1;L1b=R1;L2=R2;L3=R3");
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin));
+ CHECK(DescribeSorted(RunJoin(left, right, L"id", L"id", eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L1b=R1;L2=R2;L3=R3");
+
+ // Duplicate primary keys and multiple secondary matches for the same key.
+ FakeTable multiRight = MakeRightIntTable(
+ { { 1, L"R1a" }, { 1, L"R1b" }, { 2, L"R2" }, { 3, L"R3" } });
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryEqualJoin));
+ CHECK(DescribeSorted(RunJoin(left, multiRight, L"id", L"id", eGwsBatchSortedBlock, eGwsQueryEqualJoin))
+ == "L1=R1a;L1=R1b;L1b=R1a;L1b=R1b;L2=R2;L3=R3");
+}
+
+TEST_CASE("GwsBatchSortedBlockLeftOuterNoMatch", "[GwsQueryEngine]")
+{
+ // A primary row with no secondary match must report exactly one NULL row, for
+ // both force-one-to-one settings. The previous cursor state machine could keep
+ // returning NULL rows for the same primary row.
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 3, L"L3" }, { 5, L"L5" } });
+ FakeTable right = MakeRightIntTable({ { 1, L"R1" }, { 5, L"R5" } });
+
+ for (int forceOneToOne = 0; forceOneToOne <= 1; ++forceOneToOne)
+ {
+ INFO("forceOneToOne=" << forceOneToOne);
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", eGwsBatchSortedBlock,
+ eGwsQueryLeftOuterJoin, forceOneToOne != 0))
+ == "L1=R1;L3=NULL;L5=R5");
+ }
+}
+
+TEST_CASE("GwsBatchSortedBlockIgnoresSecondaryOrder", "[GwsQueryEngine]")
+{
+ // The batch only ever used the secondary's order as an implicit matching
+ // contract. It now sorts the block itself, so a provider that returns rows in
+ // an order that disagrees with the engine's key comparison is still correct.
+ FakeTable left = MakeIntTable(L"LeftClass",
+ { { 1, L"L1" }, { 2, L"L2" }, { 3, L"L3" }, { 4, L"L4" }, { 5, L"L5" } });
+ FakeTable right = MakeRightIntTable(
+ { { 5, L"R5" }, { 4, L"R4" }, { 3, L"R3" }, { 2, L"R2" }, { 1, L"R1" } });
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", eGwsBatchSortedBlock, eGwsQueryEqualJoin))
+ == "L1=R1;L2=R2;L3=R3;L4=R4;L5=R5");
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"id", L"id", eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=R2;L3=R3;L4=R4;L5=R5");
+}
+
+// A provider collation that differs from the engine's wcscmp key comparison
+// (ticket #502). The secondary rows are ordered case-insensitively
+// (A001..A015, a016, A017...) and the IN filter is case-insensitive. The engine
+// sorts the materialised rows with its own comparator and matches exact values,
+// so "a016" stays unmatched while A017/A018 keep their matches.
+TEST_CASE("GwsBatchSortedBlockCaseOrderingMismatch", "[GwsQueryEngine]")
+{
+ FakeTable left = MakeStringTable(L"LeftClass",
+ {
+ { L"A001", L"L1" },
+ { L"A002", L"L2" },
+ { L"A015", L"L15" },
+ { L"A016", L"L16" },
+ { L"A017", L"L17" },
+ { L"A018", L"L18" }
+ });
+
+ FakeTable right;
+ right.ClassName = L"RightClass";
+ right.CaseInsensitiveFilter = true;
+ right.Columns.push_back(FakeColumn(L"code", FdoDataType_String, true));
+ right.Columns.push_back(FakeColumn(L"label", FdoDataType_String, false));
+
+ // A case-insensitive provider orders these this way.
+ right.Rows.push_back({ FakeValue::String(L"A001"), FakeValue::String(L"R1") });
+ right.Rows.push_back({ FakeValue::String(L"A002"), FakeValue::String(L"R2") });
+ right.Rows.push_back({ FakeValue::String(L"A015"), FakeValue::String(L"R15") });
+ right.Rows.push_back({ FakeValue::String(L"a016"), FakeValue::String(L"R16") });
+ right.Rows.push_back({ FakeValue::String(L"A017"), FakeValue::String(L"R17") });
+ right.Rows.push_back({ FakeValue::String(L"A018"), FakeValue::String(L"R18") });
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryEqualJoin));
+ CHECK(Describe(RunJoin(left, right, L"code", L"code", eGwsBatchSortedBlock, eGwsQueryEqualJoin))
+ == "L1=R1;L2=R2;L15=R15;L17=R17;L18=R18");
+
+ INFO(MethodAndTypeName(eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin));
+ CHECK(Describe(RunJoin(left, right, L"code", L"code", eGwsBatchSortedBlock, eGwsQueryLeftOuterJoin))
+ == "L1=R1;L2=R2;L15=R15;L16=NULL;L17=R17;L18=R18");
+}
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-11-03 04:01:24 UTC (rev 10251)
@@ -35,6 +35,7 @@
"[DrawingService]",
"[FeatureService]",
"[Geometry]",
+ "[GwsQueryEngine]",
"[KmlService]",
"[LogManager]",
"[MappingService]",
@@ -73,6 +74,7 @@
ACE_OS::printf(" DrawingService\n");
ACE_OS::printf(" FeatureService\n");
ACE_OS::printf(" Geometry\n");
+ ACE_OS::printf(" GwsQueryEngine\n");
ACE_OS::printf(" KmlService\n");
ACE_OS::printf(" LogManager\n");
ACE_OS::printf(" MappingService\n");
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-03 03:52:01 UTC (rev 10250)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-03 04:01:24 UTC (rev 10251)
@@ -94,7 +94,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
- <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+ <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;..\Gws\Include;..\Gws\GwsCommon\inc;..\Gws\GwsQueryEngine\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Async</ExceptionHandling>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
@@ -120,7 +120,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<Optimization>Disabled</Optimization>
- <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+ <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;..\Gws\Include;..\Gws\GwsCommon\inc;..\Gws\GwsQueryEngine\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Async</ExceptionHandling>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
@@ -146,7 +146,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
- <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+ <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;..\Gws\Include;..\Gws\GwsCommon\inc;..\Gws\GwsQueryEngine\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Async</ExceptionHandling>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
@@ -173,7 +173,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
- <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+ <AdditionalIncludeDirectories>..\Common;..\Common\Cache;..\Common\Manager;..\Services\Feature;..\Services\Kml;..\Services\Mapping;..\Services\Rendering;..\Services\Resource;..\Services\ServerAdmin;..\Services\Site;..\Services\Tile;..\..\..\Common\Foundation;..\..\..\Common\Geometry;..\..\..\Common\PlatformBase;..\..\..\Common\MapGuideCommon;..\..\..\Common\MdfModel;..\..\..\Common\MdfParser;..\..\..\Common\Renderers;..\..\..\Common\Stylization;..\..\..\Oem\ACE\ACE_wrappers;..\..\..\Oem\catch2;..\..\..\Oem\dbxml\xerces-c-src\src;..\..\..\Oem\FDO\inc;..\Gws\Include;..\Gws\GwsCommon\inc;..\Gws\GwsQueryEngine\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Async</ExceptionHandling>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
@@ -208,6 +208,7 @@
<ClCompile Include="TestDrawingService.cpp" />
<ClCompile Include="TestFeatureService.cpp" />
<ClCompile Include="TestGeometry.cpp" />
+ <ClCompile Include="TestGwsQueryEngine.cpp" />
<ClCompile Include="TestKmlService.cpp" />
<ClCompile Include="TestLogManager.cpp" />
<ClCompile Include="TestLogManagerThread.cpp" />
@@ -235,6 +236,14 @@
<ResourceCompile Include="UnitTesting.rc" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\Gws\GwsCommon\GwsCommon.vcxproj">
+ <Project>{DB2CC2E3-4754-4932-B865-424A44E58FD0}</Project>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ </ProjectReference>
+ <ProjectReference Include="..\Gws\GwsQueryEngine\GwsQueryEngine.vcxproj">
+ <Project>{8D947123-A12C-45DA-BD00-8B62DD5A0459}</Project>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ </ProjectReference>
<ProjectReference Include="..\..\..\Common\Foundation\Foundation.vcxproj">
<Project>{a82adc7d-4da4-42f2-9bf6-df5dcfb44425}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
More information about the mapguide-commits
mailing list