[mapguide-commits] r10237 - in branches/4.0/MgDev: Common/MapGuideCommon/System Common/PlatformBase/MapLayer Portable/MgPortable/System Server/src/Core Server/src/UnitTesting cmake/configs
svn_mapguide at osgeo.org
svn_mapguide at osgeo.org
Sun Nov 1 20:48:52 PST 2026
Author: jng
Date: 2026-11-01 20:48:51 -0800 (Sun, 01 Nov 2026)
New Revision: 10237
Added:
branches/4.0/MgDev/Server/src/UnitTesting/TestSelection.cpp
Modified:
branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.cpp
branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h
branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.cpp
branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.h
branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.cpp
branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h
branches/4.0/MgDev/Server/src/Core/serverconfig.ini
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
branches/4.0/MgDev/cmake/configs/serverconfig.ini.in
Log:
Escape selection identity values and batch selection filters by term count
Rendering a selection turns its identity values into a disjunction with one term per
selected feature, built in MgSelectionBase::GenerateFilters(). Two things were wrong with
how that text was built. A string identity value was copied between single quotes
unchanged, so a value containing an apostrophe produced a filter FDO rejects with "String
does not represent a valid filter" and the selection was not rendered at all. And the batch
size was compared against the length of the filter text rather than against the number of
terms, so how deep a filter got depended on the identity column and not on the batch size:
the 50,000-character default carried about 2,200 "(PRIMARYINDEX=1928)"-sized terms but over
4,000 "(ID=1)" ones. The flush could also run while the filter was still empty, when one
term alone exceeded the budget, and ExecuteFeatureQuery() reads an empty override filter as
"no override" and renders the whole layer.
The depth itself is FDO's to handle now: r8418 (FDO #1019) translates a SQLite disjunction
above 900 terms as an IN list or a balanced tree, makes OptimizeFilter return a filter
without a spatial condition as it is instead of rebuilding it, and tears a filter tree down
through a worklist. What MapGuide still owns is the shape of the text it hands over, and
that is what this change makes predictable.
* Common/PlatformBase/MapLayer/SelectionBase.cpp: FDO's lexer reads a doubled quote inside
a quoted literal or identifier as one literal quote character (FdoLex::get_string), so
an identity value's apostrophe, and a quote inside a property name that has to be
delimited, are doubled now. A value without a quote, and a name without one that needs
delimiting, produce exactly the text they did before.
* Common/PlatformBase/MapLayer/SelectionBase.cpp: GenerateFilters() budgets by terms
rather than by characters, and a batch is capped at 1000 terms whatever size is asked
for (MG_MAX_SELECTION_FILTER_TERMS) - SQLite rejects any expression deeper than
SQLITE_MAX_EXPR_DEPTH (1000) and the other providers emit SQL of the same depth. The cap
bounds one filter and never the selection: a larger selection is spread over as many
filters as it takes. A configured value below the cap still applies, and a batch size of
zero or less still yields a single filter, which GenerateFilter() relies on. Because a
filter is only flushed once it holds a term, the empty-filter case is gone, and the
50,000-character reserve() goes with the character budget.
* Common/PlatformBase/MapLayer/SelectionBase.cpp: GetExtents() passes the same constant
instead of the hard-coded 30000 that was there.
* Common/PlatformBase/MapLayer/SelectionBase.h: \remarks on GenerateFilters() and
GenerateFilter() say that neither imposes a ceiling on how many features a selection may
contain, and what the batch size controls.
* Common/MapGuideCommon/System/ConfigProperties.cpp and
Portable/MgPortable/System/ConfigProperties.cpp: RenderSelectionBatchSize now means
features per filter, so its default drops from 50,000 to 1000 and its minimum from 1000
to 1, which leaves the knob able to tighten below the cap. Lowering a minimum cannot
invalidate an existing configuration; raising one would have stopped a server that still
has 50000 in its ini from starting. The constant is published through SWIG, so the PHP
API dump records the previous default until its next snapshot run.
* Common/MapGuideCommon/System/ConfigProperties.h and
Portable/MgPortable/System/ConfigProperties.h: the "/// value(10000)" annotation beside
that constant had been wrong for some time and now matches at 1000.
* cmake/configs/serverconfig.ini.in and Server/src/Core/serverconfig.ini: document the
property as features per filter, say outright that it is not a limit on how many features
may be selected - a larger selection is fetched in several queries and rendered in full -
and note that values above 1000 are treated as 1000.
* Server/src/UnitTesting/TestSelection.cpp (new), together with the CMakeLists.txt,
UnitTesting.vcxproj and UnitTesting.cpp entries: a [Selection] suite of four cases and 37
assertions. It builds its layers from a class definition rather than a feature source, so
it needs no map or database, and it covers quoting of identity values and of delimited
property names (each filter is taken through FdoFilter::Parse, since an FDO-rejected
filter is the defect), term batching at, above and below the cap, that no feature is lost
or repeated across batches, and that a composite identity stays a conjunction per feature.
Verified on ubuntu22 x64 in Debug and Release. mgserver test Selection passes in both - 37
assertions in 4 test cases, no LeakSanitizer records. RenderingService, MappingService and
Misc, which render or measure selections, pass in Debug, and RenderingService in Release.
With the SelectionBase.cpp change reverted the four cases all fail: FDO rejects
"(Name='O'Brien')" and "("Odd "Name""='x')", and 2,500 identities at a budget of 1000, 50000
and 250 came out as 32, 1 and 127 filters instead of 3, 3 and 10.
Fixed #2751
Modified: branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.cpp
===================================================================
--- branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.cpp 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.cpp 2026-11-02 04:48:51 UTC (rev 10237)
@@ -64,7 +64,7 @@
#define MG_CONFIG_MIN_JOIN_QUERY_BATCH_SIZE 1
#define MG_CONFIG_MAX_JOIN_QUERY_BATCH_SIZE 10000
-#define MG_CONFIG_MIN_RENDER_SELECTION_SIZE 1000
+#define MG_CONFIG_MIN_RENDER_SELECTION_SIZE 1
#define MG_CONFIG_MAX_RENDER_SELECTION_SIZE 100000
///////////////////////////////////////////////////////////////////////////////
@@ -309,7 +309,7 @@
const STRING MgConfigProperties::RenderingServicePropertyRasterGridSizeOverrideRatioForPlot = L"RasterGridSizeOverrideRatioForPlot";
const double MgConfigProperties::DefaultRenderingServicePropertyRasterGridSizeOverrideRatioForPlot = 0.10;
const STRING MgConfigProperties::RenderingServicePropertyRenderSelectionBatchSize = L"RenderSelectionBatchSize";
-const INT32 MgConfigProperties::DefaultRenderingServicePropertyRenderSelectionBatchSize = 50000;
+const INT32 MgConfigProperties::DefaultRenderingServicePropertyRenderSelectionBatchSize = 1000;
const STRING MgConfigProperties::RenderingServicePropertyClampPoints = L"ClampPoints";
const bool MgConfigProperties::DefaultRenderingServicePropertyClampPoints = false;
const STRING MgConfigProperties::RenderingServicePropertyGeneralizeData = L"GeneralizeData";
Modified: branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h
===================================================================
--- branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h 2026-11-02 04:48:51 UTC (rev 10237)
@@ -399,7 +399,7 @@
/// Sets the batch size for rendering a selection
static const STRING RenderingServicePropertyRenderSelectionBatchSize; /// value("RenderSelectionBatchSize")
- static const INT32 DefaultRenderingServicePropertyRenderSelectionBatchSize; /// value(10000)
+ static const INT32 DefaultRenderingServicePropertyRenderSelectionBatchSize; /// value(1000)
/// Sets the clamp points renderer option for AGG
static const STRING RenderingServicePropertyClampPoints; /// value("ClampPoints")
Modified: branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.cpp
===================================================================
--- branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.cpp 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.cpp 2026-11-02 04:48:51 UTC (rev 10237)
@@ -514,6 +514,33 @@
return bContains;
}
+// FDO's lexer reads a doubled quote inside a quoted string or identifier as one literal quote
+// character (FdoLex::get_string), so an identity value or a property name that contains one has
+// to be escaped this way. Left as it is, the filter is rejected with "String does not represent
+// a valid filter" and the selection is not rendered at all.
+static void AppendQuoted(REFSTRING dest, CREFSTRING text, wchar_t quote)
+{
+ dest.append(1, quote);
+ for (STRING::const_iterator it = text.begin(); it != text.end(); ++it)
+ {
+ if (*it == quote)
+ dest.append(1, quote);
+ dest.append(1, *it);
+ }
+ dest.append(1, quote);
+}
+
+// A selection is rendered by turning its identity values into a disjunction with one term per
+// selected feature, so the number of terms, not the length of their text, is what decides how
+// deep the resulting filter tree gets. The batch size therefore counts terms, and a batch is
+// capped at this many of them whatever size is asked for: SQLite rejects an expression deeper
+// than SQLITE_MAX_EXPR_DEPTH (1000), and the other providers emit SQL of the same depth. The
+// cap bounds one filter, never the selection -- a selection of any size is spread over as many
+// filters as it takes, and every selected feature lands in exactly one of them. A smaller batch
+// size still applies, and a caller that asks for no batching at all (a batch size of zero or
+// less, which GenerateFilter relies on) gets the whole selection in one filter.
+static const INT32 MG_MAX_SELECTION_FILTER_TERMS = 1000;
+
///////////////////////////////////////////////////////////////////////////////
/// \brief
/// Generate a collection of FDO filter strings for the selections
@@ -522,10 +549,8 @@
MgStringCollection* MgSelectionBase::GenerateFilters(MgLayerBase* layer,
CREFSTRING className, INT32 selectionSize)
{
- const INT32 OR_TEXT_SIZE = 4; // ie: " OR "
Ptr<MgStringCollection> filters;
STRING filter;
- bool filterReserved = false;
MG_TRY()
@@ -536,11 +561,20 @@
if (0 != selList && idList.size() > 0)
{
+ // Number of terms (features) to put in one filter
+ INT32 maxTerms = (INT32)selList->size();
+ if (selectionSize > 0)
+ {
+ if (selectionSize < maxTerms)
+ maxTerms = selectionSize;
+ if (maxTerms > MG_MAX_SELECTION_FILTER_TERMS)
+ maxTerms = MG_MAX_SELECTION_FILTER_TERMS;
+ }
+
+ INT32 termCount = 0;
STRING selText;
selText.reserve(128);
- bool bFirstSel = true;
-
SelectionList::iterator sIter;
for (sIter = selList->begin(); sIter != selList->end(); ++sIter)
{
@@ -559,7 +593,7 @@
if(idIter->name.find(L" ") != STRING::npos)
{
- selText.append(L"\"" + idIter->name + L"\"");
+ AppendQuoted(selText, idIter->name, L'"');
}
else
{
@@ -631,9 +665,7 @@
{
STRING strId;
m_stream->GetNullTermString(strId);
- selText.append(L"'");
- selText.append(strId);
- selText.append(L"'");
+ AppendQuoted(selText, strId, L'\'');
}
break;
case MgPropertyType::Single:
@@ -682,32 +714,23 @@
selText.append(L")");
- if (filterReserved == false)
+ // Start a new filter once this one holds its full share of terms
+ if (termCount == maxTerms)
{
- size_t listSize = (selectionSize > 0) ? selectionSize : selList->size();
-
- filter.reserve(listSize * selText.length() * 2);
- filterReserved = true;
- }
-
- if ((selectionSize > 0) && (((INT32)filter.length() + (INT32)selText.length() + OR_TEXT_SIZE) >= selectionSize))
- {
filters->Add(filter);
filter.clear();
+ termCount = 0;
}
- else
+
+ // Check if the "OR" needs to be added
+ if (termCount > 0)
{
- // Check if the "OR" needs to be added
- if (!bFirstSel)
- {
- filter.append(L" OR ");
- }
-
- bFirstSel = false;
+ filter.append(L" OR ");
}
// Add the selection
filter.append(selText);
+ ++termCount;
}
if (!filter.empty())
@@ -820,7 +843,7 @@
STRING clsName = layer->GetFeatureClassName();
STRING geomName = layer->GetFeatureGeometryName();
- Ptr<MgStringCollection> filters = GenerateFilters(layer, clsName, 30000 /* magic number */);
+ Ptr<MgStringCollection> filters = GenerateFilters(layer, clsName, MG_MAX_SELECTION_FILTER_TERMS);
INT32 numFilter = (NULL == filters)? 0 : filters->GetCount();
for (INT32 j = 0; j < numFilter; ++j)
Modified: branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.h
===================================================================
--- branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.h 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Common/PlatformBase/MapLayer/SelectionBase.h 2026-11-02 04:48:51 UTC (rev 10237)
@@ -385,6 +385,11 @@
/// Generates an Fdo filter text for the selections of the
/// specified layer and class
///
+ /// \remarks
+ /// The whole selection goes into a single filter, so the filter grows with the size of the
+ /// selection; no feature is left out whatever the size. Use GenerateFilters() to split a
+ /// large selection across several filters instead.
+ ///
/// <!-- Syntax in .Net, Java, and PHP -->
/// \htmlinclude DotNetSyntaxTop.html
/// string GenerateFilter(MgLayerBase layer, string className);
@@ -403,6 +408,14 @@
/// Generates a collection of FDO filter strings for the selections
/// of the specified layer and class
///
+ /// \remarks
+ /// selectionSize is not a limit on how many features a selection may contain. It is the
+ /// number of selected features -- that is, of terms in the disjunction -- to put in one
+ /// filter: a larger selection is split across several filters and every selected feature
+ /// is covered by exactly one of them. A batch holds at most 1000 terms whatever value is
+ /// given, and lowering it below that produces more, smaller filters. A value of zero or
+ /// less puts the whole selection in a single filter.
+ ///
/// <!-- Syntax in .Net, Java, and PHP -->
/// \htmlinclude DotNetSyntaxTop.html
/// string GenerateFilters(MgLayerBase layer, string className, int selectionSize);
Modified: branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.cpp
===================================================================
--- branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.cpp 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.cpp 2026-11-02 04:48:51 UTC (rev 10237)
@@ -74,7 +74,7 @@
#define MG_CONFIG_MIN_JOIN_QUERY_BATCH_SIZE 1
#define MG_CONFIG_MAX_JOIN_QUERY_BATCH_SIZE 10000
-#define MG_CONFIG_MIN_RENDER_SELECTION_SIZE 1000
+#define MG_CONFIG_MIN_RENDER_SELECTION_SIZE 1
#define MG_CONFIG_MAX_RENDER_SELECTION_SIZE 100000
///////////////////////////////////////////////////////////////////////////////
@@ -231,7 +231,7 @@
const STRING MgPortableConfigProperties::RenderingServicePropertyRasterGridSizeOverrideRatioForPlot = L"RasterGridSizeOverrideRatioForPlot";
const double MgPortableConfigProperties::DefaultRenderingServicePropertyRasterGridSizeOverrideRatioForPlot = 0.10;
const STRING MgPortableConfigProperties::RenderingServicePropertyRenderSelectionBatchSize = L"RenderSelectionBatchSize";
-const INT32 MgPortableConfigProperties::DefaultRenderingServicePropertyRenderSelectionBatchSize = 50000;
+const INT32 MgPortableConfigProperties::DefaultRenderingServicePropertyRenderSelectionBatchSize = 1000;
const STRING MgPortableConfigProperties::RenderingServicePropertyClampPoints = L"ClampPoints";
const bool MgPortableConfigProperties::DefaultRenderingServicePropertyClampPoints = false;
const STRING MgPortableConfigProperties::RenderingServicePropertyGeneralizeData = L"GeneralizeData";
Modified: branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h
===================================================================
--- branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h 2026-11-02 04:48:51 UTC (rev 10237)
@@ -232,7 +232,7 @@
/// Sets the batch size for rendering a selection
static const STRING RenderingServicePropertyRenderSelectionBatchSize; /// value("RenderSelectionBatchSize")
- static const INT32 DefaultRenderingServicePropertyRenderSelectionBatchSize; /// value(10000)
+ static const INT32 DefaultRenderingServicePropertyRenderSelectionBatchSize; /// value(1000)
/// Sets the clamp points renderer option for AGG
static const STRING RenderingServicePropertyClampPoints; /// value("ClampPoints")
Modified: branches/4.0/MgDev/Server/src/Core/serverconfig.ini
===================================================================
--- branches/4.0/MgDev/Server/src/Core/serverconfig.ini 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Server/src/Core/serverconfig.ini 2026-11-02 04:48:51 UTC (rev 10237)
@@ -345,7 +345,15 @@
# value.
# Change this setting to 1 to disable the
# override.
-# RenderSelectionBatchSize The batch size to use when rendering a selection
+# RenderSelectionBatchSize The number of selected features to fetch in one query
+# when rendering a selection. This is NOT a limit on how
+# many features may be selected: a larger selection is
+# fetched in several queries and rendered in full, so any
+# number of features can be selected. The selection is
+# turned into a filter with one term per feature, so this
+# bounds the size of one such filter; values above 1000
+# are treated as 1000. Lower it if a provider or database
+# struggles with a long filter.
# ClampPoints Specifies whether point coordinates are clamped
# to integer values before passing them to the
# AGG renderer (experimental)
@@ -371,7 +379,7 @@
RasterGridSizeForPlot = 50
MinRasterGridSizeForPlot = 5
RasterGridSizeOverrideRatioForPlot = 0.10
-RenderSelectionBatchSize = 50000
+RenderSelectionBatchSize = 1000
ClampPoints = 0
GeneralizeData = 0
MaxRasterImageWidth = 2048
Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-11-02 04:48:51 UTC (rev 10237)
@@ -33,6 +33,7 @@
TestMappingService.cpp
TestMdfModel.cpp
TestRasterStylization.cpp
+ TestSelection.cpp
TestMisc.cpp
TestPerformance.cpp
TestRenderingService.cpp
Added: branches/4.0/MgDev/Server/src/UnitTesting/TestSelection.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestSelection.cpp (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestSelection.cpp 2026-11-02 04:48:51 UTC (rev 10237)
@@ -0,0 +1,240 @@
+//
+// Copyright (C) 2004-2026 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
+//
+
+#include "MapGuideCommon.h"
+#include "catch.hpp"
+
+#include <Fdo.h>
+#include <utility>
+#include <vector>
+
+static const STRING SELECTION_CLASS = L"Default:Parcel";
+
+// A layer that is attached to neither a map nor a resource. Generating a selection filter only
+// asks the layer for its identity property list and for an object id, and MgLayer makes the
+// object id itself, so nothing here needs a feature source.
+static Ptr<MgLayer> CreateDetachedLayer(const std::vector<std::pair<STRING, INT32> >& idProps)
+{
+ Ptr<MgResourceIdentifier> layerId = new MgResourceIdentifier(L"Library://UnitTests/Layers/Selection.LayerDefinition");
+ Ptr<MgLayer> layer = new MgLayer(layerId, NULL, false, false);
+
+ Ptr<MgClassDefinition> cls = new MgClassDefinition();
+ Ptr<MgPropertyDefinitionCollection> ids = cls->GetIdentityProperties();
+ for (size_t i = 0; i < idProps.size(); i++)
+ {
+ Ptr<MgDataPropertyDefinition> prop = new MgDataPropertyDefinition(idProps[i].first);
+ prop->SetDataType(idProps[i].second);
+ ids->Add(prop);
+ }
+
+ layer->PopulateIdentityProperties(cls);
+ return layer;
+}
+
+static INT32 CountTerms(CREFSTRING filter)
+{
+ INT32 terms = 1;
+ STRING::size_type pos = filter.find(L" OR ");
+ while (pos != STRING::npos)
+ {
+ ++terms;
+ pos = filter.find(L" OR ", pos + 4);
+ }
+ return terms;
+}
+
+// A filter MapGuide generates has to be one FDO accepts. One it rejects makes the whole
+// selection query fail with "String does not represent a valid filter" and nothing is rendered.
+static void AssertValidFilter(CREFSTRING filter)
+{
+ REQUIRE(!filter.empty());
+
+ try
+ {
+ FdoPtr<FdoFilter> parsed = FdoFilter::Parse(filter.c_str());
+ CHECK(parsed.p != NULL);
+ }
+ catch (FdoException* e)
+ {
+ e->Release();
+ FAIL("FDO rejected a filter generated from a selection");
+ }
+}
+
+TEST_CASE("SelectionFilterEscapesQuotes", "[Selection]")
+{
+ try
+ {
+ std::vector<std::pair<STRING, INT32> > props;
+ props.push_back(std::make_pair(STRING(L"Name"), MgPropertyType::String));
+ Ptr<MgLayer> layer = CreateDetachedLayer(props);
+
+ Ptr<MgSelectionBase> selection = new MgSelectionBase();
+ selection->AddFeatureIdString(layer, SELECTION_CLASS, L"O'Brien");
+ selection->AddFeatureIdString(layer, SELECTION_CLASS, L"Plain");
+
+ STRING filter = selection->GenerateFilter(layer, SELECTION_CLASS);
+
+ // A quote inside a quoted literal is doubled, which is how FdoLex reads it back
+ CHECK(filter == L"(Name='O''Brien') OR (Name='Plain')");
+ AssertValidFilter(filter);
+ }
+ catch (MgException* e)
+ {
+ e->Release();
+ FAIL("Generating a selection filter threw");
+ }
+}
+
+TEST_CASE("SelectionFilterEscapesQuotedPropertyNames", "[Selection]")
+{
+ try
+ {
+ std::vector<std::pair<STRING, INT32> > props;
+ props.push_back(std::make_pair(STRING(L"Odd \"Name\""), MgPropertyType::String));
+ Ptr<MgLayer> layer = CreateDetachedLayer(props);
+
+ Ptr<MgSelectionBase> selection = new MgSelectionBase();
+ selection->AddFeatureIdString(layer, SELECTION_CLASS, L"x");
+
+ STRING filter = selection->GenerateFilter(layer, SELECTION_CLASS);
+
+ CHECK(filter == L"(\"Odd \"\"Name\"\"\"='x')");
+ AssertValidFilter(filter);
+ }
+ catch (MgException* e)
+ {
+ e->Release();
+ FAIL("Generating a selection filter threw");
+ }
+}
+
+TEST_CASE("SelectionFilterBatchesByTermCount", "[Selection]")
+{
+ const INT32 COUNT = 2500;
+
+ try
+ {
+ std::vector<std::pair<STRING, INT32> > props;
+ props.push_back(std::make_pair(STRING(L"ID"), MgPropertyType::Int32));
+ Ptr<MgLayer> layer = CreateDetachedLayer(props);
+
+ Ptr<MgSelectionBase> selection = new MgSelectionBase();
+ for (INT32 i = 1; i <= COUNT; i++)
+ selection->AddFeatureIdInt32(layer, SELECTION_CLASS, i);
+
+ SECTION("The budget is a number of terms")
+ {
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, 1000);
+
+ REQUIRE(filters->GetCount() == 3);
+ CHECK(CountTerms(filters->GetItem(0)) == 1000);
+ CHECK(CountTerms(filters->GetItem(1)) == 1000);
+ CHECK(CountTerms(filters->GetItem(2)) == 500);
+
+ for (INT32 i = 0; i < filters->GetCount(); i++)
+ AssertValidFilter(filters->GetItem(i));
+ }
+
+ SECTION("A budget above the cap is capped at 1000 terms")
+ {
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, 50000);
+
+ REQUIRE(filters->GetCount() == 3);
+ CHECK(CountTerms(filters->GetItem(0)) == 1000);
+ CHECK(CountTerms(filters->GetItem(2)) == 500);
+ }
+
+ SECTION("A smaller budget tightens it")
+ {
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, 250);
+
+ REQUIRE(filters->GetCount() == 10);
+ CHECK(CountTerms(filters->GetItem(0)) == 250);
+ CHECK(CountTerms(filters->GetItem(9)) == 250);
+ }
+
+ SECTION("No budget puts the whole selection in one filter")
+ {
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, -1);
+
+ REQUIRE(filters->GetCount() == 1);
+ CHECK(CountTerms(filters->GetItem(0)) == COUNT);
+ CHECK(selection->GenerateFilter(layer, SELECTION_CLASS) == filters->GetItem(0));
+ }
+
+ SECTION("Batches neither lose nor repeat a feature")
+ {
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, 1000);
+
+ INT32 terms = 0;
+ for (INT32 i = 0; i < filters->GetCount(); i++)
+ terms += CountTerms(filters->GetItem(i));
+ CHECK(terms == COUNT);
+
+ CHECK(filters->GetItem(0).find(L"(ID=1)") == 0);
+ CHECK(filters->GetItem(0).find(L" OR ") != STRING::npos);
+
+ STRING last = filters->GetItem(filters->GetCount() - 1);
+ CHECK(last.compare(0, 4, L" OR ") != 0);
+ CHECK(last.rfind(L"(ID=2500)") == last.length() - STRING(L"(ID=2500)").length());
+ }
+ }
+ catch (MgException* e)
+ {
+ e->Release();
+ FAIL("Generating a selection filter threw");
+ }
+}
+
+// A composite identity has to stay a conjunction per feature: collapsing the identities of a
+// disjunction into one term each would match their cross product instead.
+TEST_CASE("SelectionFilterKeepsCompositeIdentityPairs", "[Selection]")
+{
+ try
+ {
+ std::vector<std::pair<STRING, INT32> > props;
+ props.push_back(std::make_pair(STRING(L"ID"), MgPropertyType::Int32));
+ props.push_back(std::make_pair(STRING(L"SUBTYPE"), MgPropertyType::Int32));
+ Ptr<MgLayer> layer = CreateDetachedLayer(props);
+
+ Ptr<MgSelectionBase> selection = new MgSelectionBase();
+ for (INT32 i = 1; i <= 4; i++)
+ {
+ Ptr<MgPropertyCollection> ids = new MgPropertyCollection();
+ Ptr<MgInt32Property> id = new MgInt32Property(L"ID", i);
+ Ptr<MgInt32Property> subtype = new MgInt32Property(L"SUBTYPE", i * 10);
+ ids->Add(id);
+ ids->Add(subtype);
+ selection->AddFeatureIds(layer, SELECTION_CLASS, ids);
+ }
+
+ Ptr<MgStringCollection> filters = selection->GenerateFilters(layer, SELECTION_CLASS, 3);
+
+ REQUIRE(filters->GetCount() == 2);
+ CHECK(filters->GetItem(0) == L"(ID=1 AND SUBTYPE=10) OR (ID=2 AND SUBTYPE=20) OR (ID=3 AND SUBTYPE=30)");
+ CHECK(filters->GetItem(1) == L"(ID=4 AND SUBTYPE=40)");
+
+ for (INT32 i = 0; i < filters->GetCount(); i++)
+ AssertValidFilter(filters->GetItem(i));
+ }
+ catch (MgException* e)
+ {
+ e->Release();
+ FAIL("Generating a selection filter threw");
+ }
+}
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-11-02 04:48:51 UTC (rev 10237)
@@ -47,6 +47,7 @@
"[ServerAdminService]",
"[ServerManager]",
"[ServiceManager]",
+ "[Selection]",
"[SiteManager]",
"[SiteService]",
"[Stylization]",
@@ -84,6 +85,7 @@
ACE_OS::printf(" ServerAdminService\n");
ACE_OS::printf(" ServerManager\n");
ACE_OS::printf(" ServiceManager\n");
+ ACE_OS::printf(" Selection\n");
ACE_OS::printf(" SiteManager\n");
ACE_OS::printf(" SiteService\n");
ACE_OS::printf(" Stylization\n");
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-02 04:48:51 UTC (rev 10237)
@@ -214,6 +214,7 @@
<ClCompile Include="TestMappingService.cpp" />
<ClCompile Include="TestMdfModel.cpp" />
<ClCompile Include="TestMisc.cpp" />
+ <ClCompile Include="TestSelection.cpp" />
<ClCompile Include="TestPerformance.cpp" />
<ClCompile Include="TestProfilingService.cpp" />
<ClCompile Include="TestRasterStylization.cpp" />
Modified: branches/4.0/MgDev/cmake/configs/serverconfig.ini.in
===================================================================
--- branches/4.0/MgDev/cmake/configs/serverconfig.ini.in 2026-10-30 12:00:47 UTC (rev 10236)
+++ branches/4.0/MgDev/cmake/configs/serverconfig.ini.in 2026-11-02 04:48:51 UTC (rev 10237)
@@ -347,7 +347,15 @@
# value.
# Change this setting to 1 to disable the
# override.
-# RenderSelectionBatchSize The batch size to use when rendering a selection
+# RenderSelectionBatchSize The number of selected features to fetch in one query
+# when rendering a selection. This is NOT a limit on how
+# many features may be selected: a larger selection is
+# fetched in several queries and rendered in full, so any
+# number of features can be selected. The selection is
+# turned into a filter with one term per feature, so this
+# bounds the size of one such filter; values above 1000
+# are treated as 1000. Lower it if a provider or database
+# struggles with a long filter.
# ClampPoints Specifies whether point coordinates are clamped
# to integer values before passing them to the
# AGG renderer (experimental)
@@ -373,7 +381,7 @@
RasterGridSizeForPlot = 50
MinRasterGridSizeForPlot = 5
RasterGridSizeOverrideRatioForPlot = 0.10
-RenderSelectionBatchSize = 50000
+RenderSelectionBatchSize = 1000
ClampPoints = 0
GeneralizeData = 0
MaxRasterImageWidth = 2048
More information about the mapguide-commits
mailing list