From svn_mapguide at osgeo.org Sun Nov 1 20:48:52 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Sun, 1 Nov 2026 20:48:52 -0800 Subject: [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 Message-ID: <20261102044853.12D221A227F@trac.osgeo.org> 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 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 filters = GenerateFilters(layer, clsName, 30000 /* magic number */); + Ptr 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. + /// /// /// \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. + /// /// /// \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 +#include +#include + +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 CreateDetachedLayer(const std::vector >& idProps) +{ + Ptr layerId = new MgResourceIdentifier(L"Library://UnitTests/Layers/Selection.LayerDefinition"); + Ptr layer = new MgLayer(layerId, NULL, false, false); + + Ptr cls = new MgClassDefinition(); + Ptr ids = cls->GetIdentityProperties(); + for (size_t i = 0; i < idProps.size(); i++) + { + Ptr 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 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 > props; + props.push_back(std::make_pair(STRING(L"Name"), MgPropertyType::String)); + Ptr layer = CreateDetachedLayer(props); + + Ptr 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 > props; + props.push_back(std::make_pair(STRING(L"Odd \"Name\""), MgPropertyType::String)); + Ptr layer = CreateDetachedLayer(props); + + Ptr 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 > props; + props.push_back(std::make_pair(STRING(L"ID"), MgPropertyType::Int32)); + Ptr layer = CreateDetachedLayer(props); + + Ptr 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 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 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 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 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 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 > props; + props.push_back(std::make_pair(STRING(L"ID"), MgPropertyType::Int32)); + props.push_back(std::make_pair(STRING(L"SUBTYPE"), MgPropertyType::Int32)); + Ptr layer = CreateDetachedLayer(props); + + Ptr selection = new MgSelectionBase(); + for (INT32 i = 1; i <= 4; i++) + { + Ptr ids = new MgPropertyCollection(); + Ptr id = new MgInt32Property(L"ID", i); + Ptr subtype = new MgInt32Property(L"SUBTYPE", i * 10); + ids->Add(id); + ids->Add(subtype); + selection->AddFeatureIds(layer, SELECTION_CLASS, ids); + } + + Ptr 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 @@ + 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 From svn_mapguide at osgeo.org Mon Nov 2 06:30:20 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 06:30:20 -0800 Subject: [mapguide-commits] r10238 - in branches/4.0/MgDev: Bindings/src/Test/Java Bindings/src/Test/Java/extlib Bindings/src/Test/Java/src/org/osgeo/mapguide/test/common Bindings/src/Test/Php Oem/DWFTK/develop/global/src/dwfcore Web/src Web/src/HttpHandler Web/src/WebTestRunner Message-ID: <20261102143020.86B861CC233@trac.osgeo.org> Author: jng Date: 2026-11-02 06:30:19 -0800 (Mon, 02 Nov 2026) New Revision: 10238 Added: branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so branches/4.0/MgDev/Web/src/WebTestRunner/CMakeLists.txt Modified: branches/4.0/MgDev/Bindings/src/Test/Java/README.txt branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/common/CommonUtility.java branches/4.0/MgDev/Bindings/src/Test/Php/TestContext.php branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.cpp branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.h branches/4.0/MgDev/Web/src/CMakeLists.txt branches/4.0/MgDev/Web/src/HttpHandler/OgcServer.cpp Log: #2493: Various fixes to support web tier test suite execution on Linux Modified: branches/4.0/MgDev/Bindings/src/Test/Java/README.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Java/README.txt 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Bindings/src/Test/Java/README.txt 2026-11-02 14:30:19 UTC (rev 10238) @@ -20,4 +20,25 @@ Unit tests results will be logged to unittest.log -The default ant build.xml properties is configured for 64-bit release configuration of MapGuide. Where applicable, you should override these properties to match your specific build configuration. \ No newline at end of file +The default ant build.xml properties is configured for 64-bit release configuration of MapGuide. Where applicable, you should override these properties to match your specific build configuration. + +Running the tests on Linux +========================== + +The suites read their .dump fixtures through sqlite4java, which needs a native library +for the platform it runs on. The Windows ones are checked in under extlib/; the Linux one +is libsqlite4java-linux-amd64.so, in the same directory, so the ant `dist` target copies it +next to MgUnitTest.jar and it only has to be on java.library.path: + + java -Djava.library.path=:/webserverextensions/lib64 \ + -jar MgUnitTest.jar + +libsqlite4java-linux-amd64.so is the official 1.0.392 artifact for x86-64, taken from +Maven Central (com.almworks.sqlite4java:libsqlite4java-linux-amd64:1.0.392, packaging +"so"); its sha1 (5a97aa1b643252eb9be0901037cb440c2e145956) matches the one Maven Central +publishes. SVN's default global-ignores matches *.so, so it has to be added with +`svn add --force`. + +run_mapguide_tests.sh --tier web --web-suite java does all of this: it builds the runner +with ant into a staged test pack, starts and stops the server, and reports the +"Tests failed/run" summary. Added: branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so =================================================================== (Binary files differ) Index: branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so 2026-11-02 14:30:19 UTC (rev 10238) Property changes on: branches/4.0/MgDev/Bindings/src/Test/Java/extlib/libsqlite4java-linux-amd64.so ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +application/x-sharedlib \ No newline at end of property Modified: branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/common/CommonUtility.java =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/common/CommonUtility.java 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/common/CommonUtility.java 2026-11-02 14:30:19 UTC (rev 10238) @@ -701,7 +701,11 @@ Console.WriteLine("Read path from current directory"); //System.out.println("Param (" + paramName+ "): " + str); - String relPath = str.replaceAll("../../TestData", "").replaceAll("..\\\\..\\\\TestData", ""); + // The dumps were recorded on Windows, so the relative path under TestData can + // use either separator. Normalising before stripping the prefix is not + // optional: the remainder below the prefix keeps its separators, and a + // backslash is not one on Linux. + String relPath = str.replace('\\', '/').replace("../../TestData", ""); String resolved = TestEnvConfiguration.resolveTestDataPath(relPath, true); //System.out.println("Resolves to: " + resolved); if (resolved != null) Modified: branches/4.0/MgDev/Bindings/src/Test/Php/TestContext.php =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Php/TestContext.php 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Bindings/src/Test/Php/TestContext.php 2026-11-02 14:30:19 UTC (rev 10238) @@ -51,8 +51,12 @@ if (strlen($relPath) === 0) { return Utils::GetPath($relPath); } + // The dumps were recorded on Windows, so the relative path under TestData can use + // either separator. Normalising before stripping the prefix is not optional: the + // remainder below the prefix keeps its separators, and a backslash is not one on + // Linux. + $relPath = str_replace("\\", "/", $relPath); $relPathFixed = str_replace("../../TestData", $this->testDataRootPath, $relPath); - $relPathFixed = str_replace("..\\..\\TestData", $this->testDataRootPath, $relPathFixed); return $relPathFixed; } public function getDataFilePath($relPath, $throwIfNotFound = true) { Modified: branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.cpp =================================================================== --- branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.cpp 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.cpp 2026-11-02 14:30:19 UTC (rev 10238) @@ -24,8 +24,46 @@ #include "dwfcore/Exception.h" using namespace DWFCore; +// The message buffer is _zMessage[512]; one element is the terminator. Kept unsigned so the +// bounded scan below compares like with like: MSVC builds this file at /W4, and the VC 8.0 +// project turns warnings into errors. +#define _DWFCORE_EXCEPTION_MESSAGE_MAX_WCHARS 511u +// Copy a message into the fixed buffer, always leaving a complete terminating null behind. +// +// The buffer holds 512 wide characters, so at most 511 are copied and _zMessage[511] is reserved +// for the terminator. Zeroing "nBytes + 2" after the copy, which this used to do, does not +// terminate the string: nBytes is always a multiple of sizeof(wchar_t), so the two zeroed bytes +// land in the low half of the first wchar_t past the copy and the uninitialised high half makes +// it non-zero. Every wcslen() on the message then runs off the end of the exception object - and +// the first one to do so is the copy constructor below, reached by `throw e;` on a caught +// exception (AddressSanitizer: heap-buffer-overflow in __interceptor_wcslen). +// +// The scan is bounded by the buffer's capacity rather than by wcslen(), so it cannot over-read +// the source either: the source of a copy is another exception's buffer, which is the one this +// code has to be able to trust. +static void +dwfCopyExceptionMessage( wchar_t* pBuffer, const wchar_t* zMessage ) +{ + size_t nChars = 0; + + if (zMessage != NULL) + { + while (nChars < _DWFCORE_EXCEPTION_MESSAGE_MAX_WCHARS && zMessage[nChars] != 0) + { + ++nChars; + } + + if (nChars > 0) + { + DWFCORE_COPY_MEMORY( pBuffer, zMessage, nChars * sizeof(wchar_t) ); + } + } + + pBuffer[nChars] = 0; +} + _DWFCORE_API DWFException::DWFException( const wchar_t* zMessage, const char* zFunction, @@ -36,15 +74,7 @@ , _zFile( zFile ) , _nLine( nLine ) { - _zMessage[0] = 0; - - if (zMessage) - { - size_t nBytes = min( 511*sizeof(wchar_t), DWFCORE_WIDE_STRING_LENGTH_IN_BYTES(zMessage) ); - - DWFCORE_ZERO_MEMORY( _zMessage, nBytes + 2 ); - DWFCORE_COPY_MEMORY( _zMessage, zMessage, nBytes ); - } + dwfCopyExceptionMessage( _zMessage, zMessage ); } _DWFCORE_API @@ -61,15 +91,7 @@ , _zFile( rEx._zFile ) , _nLine( rEx._nLine ) { - _zMessage[0] = 0; - - if (rEx._zMessage) - { - size_t nBytes = min( 511*sizeof(wchar_t), DWFCORE_WIDE_STRING_LENGTH_IN_BYTES(rEx._zMessage) ); - - DWFCORE_ZERO_MEMORY( _zMessage, nBytes + 2 ); - DWFCORE_COPY_MEMORY( _zMessage, rEx._zMessage, nBytes ); - } + dwfCopyExceptionMessage( _zMessage, rEx._zMessage ); } _DWFCORE_API @@ -81,16 +103,8 @@ _zFile = rEx._zFile; _nLine = rEx._nLine; - _zMessage[0] = 0; + dwfCopyExceptionMessage( _zMessage, rEx._zMessage ); - if (rEx._zMessage) - { - size_t nBytes = min( 511*sizeof(wchar_t), DWFCORE_WIDE_STRING_LENGTH_IN_BYTES(rEx._zMessage) ); - - DWFCORE_ZERO_MEMORY( _zMessage, nBytes + 2 ); - DWFCORE_COPY_MEMORY( _zMessage, rEx._zMessage, nBytes ); - } - return *this; } Modified: branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.h =================================================================== --- branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.h 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Oem/DWFTK/develop/global/src/dwfcore/Exception.h 2026-11-02 14:30:19 UTC (rev 10238) @@ -140,7 +140,8 @@ /// Constructor /// ///\param zMessage Describes the exception event. - /// This string is copied into a local buffer and is limited to \b 512 wide characters. + /// This string is copied into a local buffer and is limited to \b 511 wide characters + /// plus a terminating null. ///\param zFunction The function in which the event occured. The \b __func__ macro is recommended. ///\param zFile The file in which the event occured. The \b __FILE__ macro is recommended. ///\param nLine The line number in the file in which the event occured. The \b __LINE__ macro is recommended. Modified: branches/4.0/MgDev/Web/src/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Web/src/CMakeLists.txt 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Web/src/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) @@ -7,6 +7,7 @@ add_subdirectory(HttpHandler) if (NOT MG_COMMON_SUBSET_ONLY) + add_subdirectory(WebTestRunner) add_subdirectory(DevHttpServer) #add_subdirectory(CgiAgent) Modified: branches/4.0/MgDev/Web/src/HttpHandler/OgcServer.cpp =================================================================== --- branches/4.0/MgDev/Web/src/HttpHandler/OgcServer.cpp 2026-11-02 04:48:51 UTC (rev 10237) +++ branches/4.0/MgDev/Web/src/HttpHandler/OgcServer.cpp 2026-11-02 14:30:19 UTC (rev 10238) @@ -1612,8 +1612,10 @@ // Kill the UNICODE Byte Order Mark, if there is one. if(pSlurp[0] == 0xfeff) { - memcpy(pSlurp,pSlurp+1,iSizeInBytes); - iSizeInBytes -= 2; + // The source and destination overlap by one CHARTYPE here, which memcpy does not allow; + // ASan stops the process on it (memcpy-param-overlap), which is how this was found. + memmove(pSlurp,pSlurp+1,iSizeInBytes); + iSizeInBytes -= sizeof(CHARTYPE); } // Make sure the thing is null-terminated. Added: branches/4.0/MgDev/Web/src/WebTestRunner/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Web/src/WebTestRunner/CMakeLists.txt (rev 0) +++ branches/4.0/MgDev/Web/src/WebTestRunner/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) @@ -0,0 +1,49 @@ +find_package (JsonCpp REQUIRED) + +include_directories(${MG_OEM_CATCH2_INCLUDE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} + ${MG_WEB_DIR}/src/HttpHandler + ${MG_WEB_DIR}/src/WebSupport + ${MG_WEB_DIR}/src/WebApp + ${MG_COMMON_DIR}/MdfModel + ${MG_COMMON_DIR}/MdfParser + ${MG_COMMON_DIR}/Foundation + ${MG_COMMON_DIR}/Geometry + ${MG_COMMON_DIR}/PlatformBase + ${MG_COMMON_DIR}/MapGuideCommon + ${ACE_INCLUDE_DIR} + ${XERCESC_INCLUDE_DIR} + ${JsonCpp_INCLUDE_DIR} + ${FDO_INCLUDE_DIR} +) + +set(MgWebTestRunner_SRCS + GeoJsonTest.cpp + main.cpp + OgcTest.cpp + TestListener.cpp + WebLayoutTest.cpp +) + +add_executable(MgWebTestRunner ${MgWebTestRunner_SRCS}) +add_sanitizers(MgWebTestRunner) +set_target_properties( MgWebTestRunner + PROPERTIES OUTPUT_NAME "MgWebTestRunner" ) + +target_link_libraries(MgWebTestRunner + ${ACE_LIBRARY} + ${XERCESC_LIBRARIES} + ${JsonCpp_LIBRARY} + MgWebSupport${MG_VERSION_SUFFIX} + MgHttpHandler${MG_VERSION_SUFFIX} + MgWebApp${MG_VERSION_SUFFIX} + MgMapGuideCommon${MG_VERSION_SUFFIX} + MgMdfParser${MG_VERSION_SUFFIX} + MgMdfModel${MG_VERSION_SUFFIX} + MgPlatformBase${MG_VERSION_SUFFIX} + MgFoundation${MG_VERSION_SUFFIX} + MgGeometry${MG_VERSION_SUFFIX} +) + +# The suite reads its fixtures from TestData relative to the working directory +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/TestData DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) From svn_mapguide at osgeo.org Mon Nov 2 07:13:14 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 07:13:14 -0800 Subject: [mapguide-commits] r10239 - in branches/4.0/MgDev/Bindings/src/Bindings/DotNet: Foundation Geometry MapGuideCommon PlatformBase Portable Web Message-ID: <20261102151314.C07361CD307@trac.osgeo.org> Author: jng Date: 2026-11-02 07:13:14 -0800 (Mon, 02 Nov 2026) New Revision: 10239 Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt Log: #2493: Activate the .net binding targets for building on Linux. This is to facilitate e2e testing of the .net binding on Linux and nothing else Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -33,6 +33,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${FoundationUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -61,7 +68,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module FoundationUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -31,6 +31,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${GeometryUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -59,7 +66,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module GeometryUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -31,6 +31,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${MapGuideCommonUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -63,7 +70,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module MapGuideCommonUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -31,6 +31,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${PlatformBaseUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -59,7 +66,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module PlatformBaseUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -31,6 +31,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${PortableUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -63,7 +70,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module PortableUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-02 14:30:19 UTC (rev 10238) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) @@ -34,6 +34,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") +# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the +# references to SWIGWStringHelper while no longer emitting the class itself, so the +# generated code does not compile (CS0103). Letting the helper be generated is +# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so +# there is nothing for it to collide with. The other two suppressions are honoured +# consistently and stay. The Windows build sets all three from *.vcxproj and does not +# use this file. add_custom_command(OUTPUT ${WebUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -62,7 +69,7 @@ COMMAND ${CMAKE_COMMAND} -E echo Running SWIG [DotNet]. Output Dir: ${MG_DOTNET_PROJECT_DIR} COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER - -DSWIG_CSHARP_NO_STRING_HELPER -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_STRING_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module WebUnmanagedApi From svn_mapguide at osgeo.org Mon Nov 2 07:30:54 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 07:30:54 -0800 Subject: [mapguide-commits] r10240 - in branches/4.0/MgDev/Bindings/src/Bindings/DotNet: Geometry MapGuideCommon PlatformBase Web Message-ID: <20261102153054.41AD71CDA93@trac.osgeo.org> Author: jng Date: 2026-11-02 07:30:53 -0800 (Mon, 02 Nov 2026) New Revision: 10240 Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt Log: #2493: DotNet SWIG targets re-ordering to avoid file races Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-02 15:30:53 UTC (rev 10240) @@ -6,6 +6,12 @@ add_library(GeometryUnmanagedApi SHARED ${GeometryUnmanagedApi_SRCS}) add_dependencies(GeometryUnmanagedApi IMake) +# Generation is not parallel-safe. IMake writes Api_Properties.i into the source +# tree (removed again below) and each module %includes the .i files the earlier modules +# generate, so running these targets concurrently lets one module read another's +# fragments mid-write ("Unterminated %{ ... %}" in Api_Doc.i, or "Unable to find +# 'Api_Properties.i'"). Serialise in the %include order of ApiGen.xml. +add_dependencies(GeometryUnmanagedApi FoundationUnmanagedApi) if (UNIX) add_definitions(-DDOTNETCORE) # NOTE: We don't care where this source is generated because the C# source is only built on windows Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-02 15:30:53 UTC (rev 10240) @@ -6,6 +6,9 @@ add_library(MapGuideCommonUnmanagedApi SHARED ${MapGuideCommonUnmanagedApi_SRCS}) add_dependencies(MapGuideCommonUnmanagedApi IMake) +# See Geometry/CMakeLists.txt: generation is serialised in the %include order of +# MapGuideCommonApiGen.xml. +add_dependencies(MapGuideCommonUnmanagedApi FoundationUnmanagedApi GeometryUnmanagedApi PlatformBaseUnmanagedApi) if (UNIX) add_definitions(-DDOTNETCORE) # NOTE: We don't care where this source is generated because the C# source is only built on windows Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-02 15:30:53 UTC (rev 10240) @@ -6,6 +6,9 @@ add_library(PlatformBaseUnmanagedApi SHARED ${PlatformBaseUnmanagedApi_SRCS}) add_dependencies(PlatformBaseUnmanagedApi IMake) +# See Geometry/CMakeLists.txt: generation is serialised in the %include order of +# PlatformBaseApiGen.xml. +add_dependencies(PlatformBaseUnmanagedApi FoundationUnmanagedApi GeometryUnmanagedApi) if (UNIX) add_definitions(-DDOTNETCORE) # NOTE: We don't care where this source is generated because the C# source is only built on windows Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-02 15:13:14 UTC (rev 10239) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-02 15:30:53 UTC (rev 10240) @@ -9,6 +9,9 @@ add_library(WebUnmanagedApi SHARED ${WebUnmanagedApi_SRCS}) add_dependencies(WebUnmanagedApi IMake) +# See Geometry/CMakeLists.txt: generation is serialised in the %include order of +# WebApiGen.xml. +add_dependencies(WebUnmanagedApi FoundationUnmanagedApi GeometryUnmanagedApi PlatformBaseUnmanagedApi MapGuideCommonUnmanagedApi) if (UNIX) add_definitions(-DDOTNETCORE) # NOTE: We don't care where this source is generated because the C# source is only built on windows From svn_mapguide at osgeo.org Mon Nov 2 08:29:43 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 08:29:43 -0800 Subject: [mapguide-commits] r10241 - in branches/4.0/MgDev: Bindings/src/Test/DotNet/src/TestCommon/ResourceService Bindings/src/Test/Java/src/org/osgeo/mapguide/test/platform/operations/resource Bindings/src/Test/Php Common/Foundation/System Message-ID: <20261102162943.538E21CEBE0@trac.osgeo.org> Author: jng Date: 2026-11-02 08:29:42 -0800 (Mon, 02 Nov 2026) New Revision: 10241 Modified: branches/4.0/MgDev/Bindings/src/Test/DotNet/src/TestCommon/ResourceService/Operations.cs branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/platform/operations/resource/ApplyResourcePackage.java branches/4.0/MgDev/Bindings/src/Test/Php/ResourceServiceAPI.php branches/4.0/MgDev/Common/Foundation/System/ByteSourceFileImpl.cpp Log: #2493: Fix cause of ApplyResourcePackage test failure on bad filename on Linux Modified: branches/4.0/MgDev/Bindings/src/Test/DotNet/src/TestCommon/ResourceService/Operations.cs =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/DotNet/src/TestCommon/ResourceService/Operations.cs 2026-11-02 15:30:53 UTC (rev 10240) +++ branches/4.0/MgDev/Bindings/src/Test/DotNet/src/TestCommon/ResourceService/Operations.cs 2026-11-02 16:29:42 UTC (rev 10241) @@ -618,18 +618,11 @@ { try { - if (CommonUtility.IsWindows()) - { - MgByteReader reader = param.TryGetByteReader("PACKAGE", false); - - _resourceService.ApplyResourcePackage(reader); - - return TestResult.FromByteReader(null); - } - else - { - throw new Exception("FIXME: ApplyResourcePackage will kill the mgserver on invalid package files"); - } + MgByteReader reader = param.TryGetByteReader("PACKAGE", false); + + _resourceService.ApplyResourcePackage(reader); + + return TestResult.FromByteReader(null); } catch (MgException ex) { Modified: branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/platform/operations/resource/ApplyResourcePackage.java =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/platform/operations/resource/ApplyResourcePackage.java 2026-11-02 15:30:53 UTC (rev 10240) +++ branches/4.0/MgDev/Bindings/src/Test/Java/src/org/osgeo/mapguide/test/platform/operations/resource/ApplyResourcePackage.java 2026-11-02 16:29:42 UTC (rev 10241) @@ -25,16 +25,11 @@ if (param.get("PACKAGE") != null) packagePath = param.get("PACKAGE"); - if (CommonUtility.IsWindows()) { - //Console.WriteLine("\t\tPackage Path: " + packagePath); - MgByteReader reader = CommonUtility.GetByteReaderFromPath(packagePath, false); + MgByteReader reader = CommonUtility.GetByteReaderFromPath(packagePath, false); - _resourceService.applyResourcePackage(reader); + _resourceService.applyResourcePackage(reader); - return TestResult.Ok(); - } else { - throw new RuntimeException("FIXME: ApplyResourcePackage will kill the mgserver on invalid package files"); - } + return TestResult.Ok(); } catch (MgException ex) { Modified: branches/4.0/MgDev/Bindings/src/Test/Php/ResourceServiceAPI.php =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Php/ResourceServiceAPI.php 2026-11-02 15:30:53 UTC (rev 10240) +++ branches/4.0/MgDev/Bindings/src/Test/Php/ResourceServiceAPI.php 2026-11-02 16:29:42 UTC (rev 10241) @@ -789,16 +789,12 @@ $packageParam = $this->unitTestParamVm->GetString("ParamValue"); $arrayParam['PACKAGE']=$this->testContext->resolveDataFilePath($packageParam); - if (Utils::IsWindows()) { - $packageSource = new MgByteSource($arrayParam["PACKAGE"]); - $package = $packageSource->GetReader(); + $packageSource = new MgByteSource($arrayParam["PACKAGE"]); + $package = $packageSource->GetReader(); - $byteReader = $this->resourceSrvc->ApplyResourcePackage($package); + $byteReader = $this->resourceSrvc->ApplyResourcePackage($package); - return Utils::GetResponse($byteReader); - } else { - return new Result("FIXME: ApplyResourcePackage will kill the mgserver on invalid package files", "text/plain"); - } + return Utils::GetResponse($byteReader); } catch (MgException $e) { Modified: branches/4.0/MgDev/Common/Foundation/System/ByteSourceFileImpl.cpp =================================================================== --- branches/4.0/MgDev/Common/Foundation/System/ByteSourceFileImpl.cpp 2026-11-02 15:30:53 UTC (rev 10240) +++ branches/4.0/MgDev/Common/Foundation/System/ByteSourceFileImpl.cpp 2026-11-02 16:29:42 UTC (rev 10241) @@ -113,6 +113,20 @@ void ByteSourceFileImpl::LoadFile(CREFSTRING filename) { + // A directory opens successfully on POSIX, but it cannot be read (EISDIR) and the length + // SEEK_END reports for it is meaningless - LLONG_MAX on some filesystems, -1 on others. + // A reader built over one therefore declares an impossible length when it is serialized, + // and the peer reading that stream waits for data that will never arrive. Windows fails the + // open outright, so reject a directory on both platforms to keep the behaviour identical. + if (MgFileUtil::IsDirectory(filename)) + { + MgStringCollection arguments; + arguments.Add(filename); + + throw new MgException(MgExceptionCodes::MgFileIoException, L"ByteSourceFileImpl.LoadFile", + __LINE__, __WFILE__, &arguments, L"", NULL); + } + //Use ACE open insted of _wfopen to allow Linux compatibility m_file = ACE_OS::open(MG_WCHAR_TO_TCHAR(filename), 0, ACE_DEFAULT_OPEN_PERMS, 0); if(m_file == ACE_INVALID_HANDLE) From svn_mapguide at osgeo.org Mon Nov 2 15:43:24 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 15:43:24 -0800 Subject: [mapguide-commits] r10242 - in branches/4.0/MgDev: Server/src/Services/Feature Web/src/HttpHandler Web/src/WebTestRunner Message-ID: <20261102234324.208B71A1D9F@trac.osgeo.org> Author: jng Date: 2026-11-02 15:43:21 -0800 (Mon, 02 Nov 2026) New Revision: 10242 Modified: branches/4.0/MgDev/Server/src/Services/Feature/TransformCache.h branches/4.0/MgDev/Server/src/Services/Feature/WfsQueryAdapter.cpp branches/4.0/MgDev/Web/src/HttpHandler/HttpWmsGetFeatureInfo.cpp branches/4.0/MgDev/Web/src/WebTestRunner/OgcTest.cpp Log: #2894: Plug more leaks at the web tier and add REGRESS.xml.awd self-testing to WebTestRunner Modified: branches/4.0/MgDev/Server/src/Services/Feature/TransformCache.h =================================================================== --- branches/4.0/MgDev/Server/src/Services/Feature/TransformCache.h 2026-11-02 16:29:42 UTC (rev 10241) +++ branches/4.0/MgDev/Server/src/Services/Feature/TransformCache.h 2026-11-02 23:43:21 UTC (rev 10242) @@ -39,6 +39,9 @@ void SetEnvelope(MgEnvelope* extent); MgEnvelope* GetEnvelope(); + // Ownership: with reverse == true the returned entry belongs to `cache` (released by Clear); + // with reverse == false a fresh entry is built and not cached, so the caller owns it and must + // delete it. The two directions cannot share a cache key, hence the asymmetry. static TransformCache* GetLayerToMapTransform(TransformCacheMap& cache, CREFSTRING featureName, MgResourceIdentifier* resId, Modified: branches/4.0/MgDev/Server/src/Services/Feature/WfsQueryAdapter.cpp =================================================================== --- branches/4.0/MgDev/Server/src/Services/Feature/WfsQueryAdapter.cpp 2026-11-02 16:29:42 UTC (rev 10241) +++ branches/4.0/MgDev/Server/src/Services/Feature/WfsQueryAdapter.cpp 2026-11-02 23:43:21 UTC (rev 10242) @@ -111,8 +111,12 @@ } //get a transform from feature space to mapping space - TransformCache* item = TransformCache::GetLayerToMapTransform(m_transformCache, m_className, m_fs, m_mapCs, &fact, m_featSvc, false); - m_xform = item ? item->GetMgTransform() : NULL; + // A reverse == false lookup is not cached - the entries the cache holds are the map-to-feature + // direction, so both directions cannot share a key - which makes the result this call's to + // delete. The transform it carries is reference counted, so m_xform keeps what the query needs. + unique_ptr featureToMapXform( + TransformCache::GetLayerToMapTransform(m_transformCache, m_className, m_fs, m_mapCs, &fact, m_featSvc, false)); + m_xform = featureToMapXform.get() ? featureToMapXform->GetMgTransform() : NULL; assert(m_classDef.p != NULL); if (NULL == m_classDef.p) Modified: branches/4.0/MgDev/Web/src/HttpHandler/HttpWmsGetFeatureInfo.cpp =================================================================== --- branches/4.0/MgDev/Web/src/HttpHandler/HttpWmsGetFeatureInfo.cpp 2026-11-02 16:29:42 UTC (rev 10241) +++ branches/4.0/MgDev/Web/src/HttpHandler/HttpWmsGetFeatureInfo.cpp 2026-11-02 23:43:21 UTC (rev 10242) @@ -168,12 +168,18 @@ Ptr geometryFact = new MgGeometryFactory(); Ptr linearRingCoords = new MgCoordinateCollection(); - linearRingCoords->Add(new MgCoordinateXY(mcsMinX, mcsMinY)); - linearRingCoords->Add(new MgCoordinateXY(mcsMinX, mcsMaxY)); - linearRingCoords->Add(new MgCoordinateXY(mcsMaxX, mcsMaxY)); - linearRingCoords->Add(new MgCoordinateXY(mcsMaxX, mcsMinY)); - linearRingCoords->Add(new MgCoordinateXY(mcsMinX, mcsMinY)); + // Add() takes its own reference, so each of these needs a home for the one that new gives it. + Ptr mcsMinMin = new MgCoordinateXY(mcsMinX, mcsMinY); + Ptr mcsMinMax = new MgCoordinateXY(mcsMinX, mcsMaxY); + Ptr mcsMaxMax = new MgCoordinateXY(mcsMaxX, mcsMaxY); + Ptr mcsMaxMin = new MgCoordinateXY(mcsMaxX, mcsMinY); + linearRingCoords->Add(mcsMinMin); + linearRingCoords->Add(mcsMinMax); + linearRingCoords->Add(mcsMaxMax); + linearRingCoords->Add(mcsMaxMin); + linearRingCoords->Add(mcsMinMin); // closes the ring + Ptr linearRing = geometryFact->CreateLinearRing(linearRingCoords); selectionGeometry = geometryFact->CreatePolygon(linearRing, NULL); Modified: branches/4.0/MgDev/Web/src/WebTestRunner/OgcTest.cpp =================================================================== --- branches/4.0/MgDev/Web/src/WebTestRunner/OgcTest.cpp 2026-11-02 16:29:42 UTC (rev 10241) +++ branches/4.0/MgDev/Web/src/WebTestRunner/OgcTest.cpp 2026-11-02 23:43:21 UTC (rev 10242) @@ -289,4 +289,60 @@ REQUIRE((L"application/json" == result->GetResultContentType())); // This CRS string should be in the GeoJSON REQUIRE(respContent.find(L"urn:ogc:def:crs:OGC:1.3:CRS84") != STRING::npos); -} \ No newline at end of file +} + +#ifdef _DEBUG +// The WMS REGRESS template (server/Wms/REGRESS.xml.awd) is a self-test for the OGC template engine: +// it evaluates each engine feature against a constant baseline and writes or +// for every it contains. Only a Debug build honours VERSION=REGRESS (the negotiation bypass +// and the PI the template relies on are compiled out of Release), so this case runs +// only there. +static int CountOccurrences(CREFSTRING text, CREFSTRING token) +{ + int count = 0; + for (STRING::size_type pos = text.find(token); pos != STRING::npos; pos = text.find(token, pos + token.length())) + { + count++; + } + return count; +} + +TEST_CASE("Wms_GetCapabilities_OgcServerSelfTest", "[Wms]") +{ + STRING uri = L""; + Ptr req = new MgHttpRequest(uri); + Ptr param = req->GetRequestParam(); + + MgConfiguration* cfg = MgConfiguration::GetInstance(); + STRING pwd; + cfg->GetStringValue(MgConfigProperties::OgcPropertiesSection, MgConfigProperties::WmsPassword, pwd, L""); + + param->AddParameter(L"USERNAME", MgUser::WmsUser); + param->AddParameter(L"PASSWORD", pwd); + + param->AddParameter(L"SERVICE", L"WMS"); + param->AddParameter(L"REQUEST", L"GETCAPABILITIES"); + param->AddParameter(L"VERSION", L"REGRESS"); + + Ptr resp = req->Execute(); + Ptr result = resp->GetResult(); + + STRING err = result->GetDetailedErrorMessage(); + REQUIRE(err.empty()); + + REQUIRE((L"text/xml" == result->GetResultContentType())); + + Ptr reader = dynamic_cast(result->GetResultObject()); + REQUIRE(reader.p != nullptr); + STRING respContent = reader->ToString(); + + // The self-test document, not a real capabilities response, was selected + REQUIRE(respContent.find(L"") != STRING::npos); + + // Every writes exactly one or + int tests = CountOccurrences(respContent, L"") == tests); + REQUIRE(CountOccurrences(respContent, L"") == 0); +} +#endif From svn_mapguide at osgeo.org Mon Nov 2 16:09:14 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 16:09:14 -0800 Subject: [mapguide-commits] r10243 - in branches/4.0/MgDev: . Common/Stylization Portable/MgPortable Portable/UnitTest Server/src/Common/Manager Server/src/Core Server/src/Gws/GwsCommon Server/src/Gws/GwsQueryEngine Server/src/Services/Feature Server/src/Services/Mapping Server/src/UnitTesting Message-ID: <20261103000915.32CBD1A336C@trac.osgeo.org> Author: jng Date: 2026-11-02 16:09:12 -0800 (Mon, 02 Nov 2026) New Revision: 10243 Modified: branches/4.0/MgDev/Common/Stylization/Stylization.vcxproj branches/4.0/MgDev/Portable/MgPortable/MgPortable.vcxproj branches/4.0/MgDev/Portable/UnitTest/UnitTest.vcxproj branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.vcxproj branches/4.0/MgDev/Server/src/Core/ServerCore.vcxproj branches/4.0/MgDev/Server/src/Gws/GwsCommon/GwsCommon.vcxproj branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsQueryEngine.vcxproj branches/4.0/MgDev/Server/src/Services/Feature/ServerFeatureService.vcxproj branches/4.0/MgDev/Server/src/Services/Mapping/ServerMappingService.vcxproj branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj branches/4.0/MgDev/build.bat branches/4.0/MgDev/setenvironment64.bat Log: #2895: Allow for a single MapGuide source tree to build in both debug and release. This primarily requires downstream consumers of FDO use separate debug/release linker paths for .lib files. Modified: branches/4.0/MgDev/Common/Stylization/Stylization.vcxproj =================================================================== --- branches/4.0/MgDev/Common/Stylization/Stylization.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Common/Stylization/Stylization.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -138,7 +138,7 @@ FDO.lib;FDOCommon.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgStylizationd.dll - ..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true $(OutDir)MgStylizationd.pdb @@ -203,7 +203,7 @@ FDO.lib;FDOCommon.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgStylization.dll - ..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true $(OutDir)MgStylization.pdb Modified: branches/4.0/MgDev/Portable/MgPortable/MgPortable.vcxproj =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/MgPortable.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Portable/MgPortable/MgPortable.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -170,7 +170,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;FDOSpatial.lib;ExpressionEngine.lib;ACEd.lib;MgFoundationd.lib;MgGeometryd.lib;MgMdfModeld.lib;MgMdfParserd.lib;MgPlatformBased.lib;MgRenderersd.lib;MgStylizationd.lib;MgGwsCommond.lib;MgGwsQueryEngined.lib;xerces-c_3mgD.lib;dwfcore_wt.1.7.0.lib;dwftk_wt.7.7.0.lib;w3dtk_wt.1.7.1555.lib;whiptk_wt.7.13.601.lib;MgSecurityd.lib;%(AdditionalDependencies) NotSet $(OutDir)MgPortabled.dll - ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64;..\..\Oem\DWFTK\develop\global\lib\x64\static\$(Configuration)\vc10.0;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\..\Server\lib\$(Configuration)64;%(AdditionalLibraryDirectories) + ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64\$(Configuration)64;..\..\Oem\DWFTK\develop\global\lib\x64\static\$(Configuration)\vc10.0;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\..\Server\lib\$(Configuration)64;%(AdditionalLibraryDirectories) FDOd.dll;FDOCommond.dll;FDOGeometryd.dll;FDOSpatiald.dll;ExpressionEngined.dll;%(DelayLoadDLLs) true Windows @@ -209,7 +209,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;FDOSpatial.lib;ExpressionEngine.lib;ACE.lib;MgFoundation.lib;MgGeometry.lib;MgMdfModel.lib;MgMdfParser.lib;MgPlatformBase.lib;MgRenderers.lib;MgStylization.lib;MgGwsCommon.lib;MgGwsQueryEngine.lib;xerces-c_3mg.lib;dwfcore_wt.1.7.0.lib;dwftk_wt.7.7.0.lib;w3dtk_wt.1.7.1555.lib;whiptk_wt.7.13.601.lib;MgSecurity.lib;%(AdditionalDependencies) $(OutDir)$(ProjectName).dll - ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64;..\..\Oem\DWFTK\develop\global\lib\x64\static\$(Configuration)\vc10.0;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\..\Server\lib\$(Configuration)64;%(AdditionalLibraryDirectories) + ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64\$(Configuration)64;..\..\Oem\DWFTK\develop\global\lib\x64\static\$(Configuration)\vc10.0;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\..\Server\lib\$(Configuration)64;%(AdditionalLibraryDirectories) MgMdfParser.dll;MgGeometry.dll;MgGwsCommon.dll;MgGwsResource.dll;FDO.dll;FDOCommon.dll;FDOSpatial.dll;MgStylization.dll;FDOGeometry.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true Windows Modified: branches/4.0/MgDev/Portable/UnitTest/UnitTest.vcxproj =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/UnitTest.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Portable/UnitTest/UnitTest.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -134,7 +134,7 @@ MgPortabled.lib;FDO.lib;FDOCommon.lib;ACEd.lib;MgFoundationd.lib;MgGeometryd.lib;MgMdfModeld.lib;MgMdfParserd.lib;MgPlatformBased.lib;MgRenderersd.lib;MgStylizationd.lib;xerces-c_3mgD.lib;%(AdditionalDependencies) - ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64;..\..\Oem\DWFTK\develop\global\lib\static\$(Configuration)64;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\Lib\debug64 + ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64\$(Configuration)64;..\..\Oem\DWFTK\develop\global\lib\static\$(Configuration)64;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\Lib\debug64 true MachineX64 @@ -231,7 +231,7 @@ MgPortable.lib;FDO.lib;FDOCommon.lib;ACE.lib;MgFoundation.lib;MgGeometry.lib;MgMdfModel.lib;MgMdfParser.lib;MgPlatformBase.lib;MgRenderers.lib;MgStylization.lib;xerces-c_3mg.lib;%(AdditionalDependencies) - ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64;..\..\Oem\DWFTK\develop\global\lib\static\$(Configuration)64;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\Lib\release64;%(AdditionalLibraryDirectories) + ..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\Oem\FDO\Lib64\$(Configuration)64;..\..\Oem\DWFTK\develop\global\lib\static\$(Configuration)64;..\..\Oem\dbxml\xerces-c-src\Build\$(Configuration)64;..\..\Common\lib\$(Configuration)64;..\Lib\release64;%(AdditionalLibraryDirectories) FDO.dll;%(DelayLoadDLLs) true true Modified: branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -135,7 +135,7 @@ ACEd.lib;FDO.lib;FDOCommon.lib;pdh.lib;psapi.lib;%(AdditionalDependencies) $(OutDir)MgServerManagerd.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;%(DelayLoadDLLs) true $(OutDir)MgServerManagerd.pdb @@ -189,7 +189,7 @@ ACE.lib;FDO.lib;FDOCommon.lib;pdh.lib;psapi.lib;%(AdditionalDependencies) $(OutDir)MgServerManager.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;%(DelayLoadDLLs) true $(OutDir)MgServerManager.pdb Modified: branches/4.0/MgDev/Server/src/Core/ServerCore.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Core/ServerCore.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Core/ServerCore.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -135,7 +135,7 @@ ACEd.lib;xerces-c_3mgD.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)mgserver.exe - ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDOCommon.dll;%(DelayLoadDLLs) true $(OutDir)mgserver.pdb @@ -198,7 +198,7 @@ ACE.lib;xerces-c_3mg.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)mgserver.exe - ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDOCommon.dll;%(DelayLoadDLLs) true $(OutDir)mgserver.pdb Modified: branches/4.0/MgDev/Server/src/Gws/GwsCommon/GwsCommon.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsCommon/GwsCommon.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Gws/GwsCommon/GwsCommon.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -131,7 +131,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;%(AdditionalDependencies) $(OutDir)MgGwsCommond.dll - ..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;%(DelayLoadDLLs) true $(OutDir)MgGwsCommond.pdb @@ -185,7 +185,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;%(AdditionalDependencies) $(OutDir)MgGwsCommon.dll - ..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;%(DelayLoadDLLs) true $(OutDir)MgGwsCommon.pdb Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsQueryEngine.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsQueryEngine.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsQueryEngine.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -138,7 +138,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgGwsQueryEngined.dll - ..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) GwsQueryEngine.def FDO.dll;FDOCommon.dll;FDOGeometry.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true @@ -202,7 +202,7 @@ FDO.lib;FDOCommon.lib;FDOGeometry.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgGwsQueryEngine.dll - ..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) GwsQueryEngine.def FDO.dll;FDOCommon.dll;FDOGeometry.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true Modified: branches/4.0/MgDev/Server/src/Services/Feature/ServerFeatureService.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Services/Feature/ServerFeatureService.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Services/Feature/ServerFeatureService.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -135,7 +135,7 @@ ACEd.lib;xerces-c_3mgD.lib;FDO.lib;FDOCommon.lib;FDOGeometry.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgServerFeatureServiced.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true $(OutDir)MgServerFeatureServiced.pdb @@ -194,7 +194,7 @@ ACE.lib;xerces-c_3mg.lib;FDO.lib;FDOCommon.lib;FDOGeometry.lib;ExpressionEngine.lib;%(AdditionalDependencies) $(OutDir)MgServerFeatureService.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;ExpressionEngine.dll;%(DelayLoadDLLs) true $(OutDir)MgServerFeatureService.pdb Modified: branches/4.0/MgDev/Server/src/Services/Mapping/ServerMappingService.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/Services/Mapping/ServerMappingService.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/Services/Mapping/ServerMappingService.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -133,7 +133,7 @@ ACEd.lib;xerces-c_3mgD.lib;FDO.lib;FDOCommon.lib;FDOGeometry.lib;FDOSpatial.lib;%(AdditionalDependencies) $(OutDir)MgServerMappingServiced.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;FDOSpatial.dll;%(DelayLoadDLLs) true $(OutDir)MgServerMappingServiced.pdb @@ -187,7 +187,7 @@ ACE.lib;xerces-c_3mg.lib;FDO.lib;FDOCommon.lib;FDOGeometry.lib;FDOSpatial.lib;%(AdditionalDependencies) $(OutDir)MgServerMappingService.dll - ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\..\Oem\dbxml\lib64\$(Configuration);..\..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) FDO.dll;FDOCommon.dll;FDOGeometry.dll;FDOSpatial.dll;%(DelayLoadDLLs) true $(OutDir)MgServerMappingService.pdb Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) @@ -104,7 +104,7 @@ true - ACEd.lib;%(AdditionalDependencies) + ACEd.lib;FDO.lib;%(AdditionalDependencies) $(OutDir)MgUnitTestingd.dll ..\..\..\Oem\ACE\ACE_wrappers\lib\$(Configuration);..\..\..\Oem\FDO\lib;%(AdditionalLibraryDirectories) true @@ -130,9 +130,9 @@ true - ACEd.lib;%(AdditionalDependencies) + ACEd.lib;FDO.lib;%(AdditionalDependencies) $(OutDir)MgUnitTestingd.dll - ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true $(OutDir)MgUnitTestingd.pdb Windows @@ -155,7 +155,7 @@ true - ACE.lib;%(AdditionalDependencies) + ACE.lib;FDO.lib;%(AdditionalDependencies) $(OutDir)MgUnitTesting.dll ..\..\..\Oem\ACE\ACE_wrappers\lib\$(Configuration);..\..\..\Oem\FDO\lib;%(AdditionalLibraryDirectories) true @@ -182,9 +182,9 @@ true - ACE.lib;%(AdditionalDependencies) + ACE.lib;FDO.lib;%(AdditionalDependencies) $(OutDir)MgUnitTesting.dll - ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64;%(AdditionalLibraryDirectories) + ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true $(OutDir)MgUnitTesting.pdb Windows Modified: branches/4.0/MgDev/build.bat =================================================================== --- branches/4.0/MgDev/build.bat 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/build.bat 2026-11-03 00:09:12 UTC (rev 10243) @@ -14,7 +14,8 @@ rem - Bin\%CONFIG% (where %CONFIG% is debug or release, debug64 or release64 for 64-bit FDO) rem - Inc rem - Lib -rem - Lib64 (for 64-bit FDO) +rem - Lib64\%CONFIG% (for 64-bit FDO, where %CONFIG% is debug64 or release64: one +rem subdirectory per configuration, so both can be staged in the same tree) rem rem 2) Download and install the following: rem - Java SDK (http://java.sun.com) Modified: branches/4.0/MgDev/setenvironment64.bat =================================================================== --- branches/4.0/MgDev/setenvironment64.bat 2026-11-02 23:43:21 UTC (rev 10242) +++ branches/4.0/MgDev/setenvironment64.bat 2026-11-03 00:09:12 UTC (rev 10243) @@ -50,7 +50,7 @@ SET MG_OEM_DB=db-4.8.26 SET MG_FDO_BIN=%MG_OEM%\FDO\Bin\%TYPEBUILD% SET MG_FDO_INC=%MG_OEM%\FDO\Inc -SET MG_FDO_LIB=%MG_OEM%\FDO\Lib64 +SET MG_FDO_LIB=%MG_OEM%\FDO\Lib64\%TYPEBUILD% SET MG_COMMON=%MG_DEV%\Common SET MG_SERVER=%MG_DEV%\Server SET MG_WEB=%MG_DEV%\Web From svn_mapguide at osgeo.org Mon Nov 2 16:10:39 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 16:10:39 -0800 Subject: [mapguide-commits] r10244 - trunk/Installer/FdoRegUtil Message-ID: <20261103001039.4C4A21A3D0B@trac.osgeo.org> Author: jng Date: 2026-11-02 16:10:38 -0800 (Mon, 02 Nov 2026) New Revision: 10244 Modified: trunk/Installer/FdoRegUtil/FdoRegUtil.vcxproj Log: #2895: Use configuration-specific FDO libs Modified: trunk/Installer/FdoRegUtil/FdoRegUtil.vcxproj =================================================================== --- trunk/Installer/FdoRegUtil/FdoRegUtil.vcxproj 2026-11-03 00:09:12 UTC (rev 10243) +++ trunk/Installer/FdoRegUtil/FdoRegUtil.vcxproj 2026-11-03 00:10:38 UTC (rev 10244) @@ -64,7 +64,7 @@ FDO.lib;FDOCommon.lib;%(AdditionalDependencies) - ..\..\MgDev\Oem\Fdo\Lib64;%(AdditionalLibraryDirectories) + ..\..\MgDev\Oem\Fdo\Lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true Console MachineX64 @@ -87,7 +87,7 @@ FDO.lib;FDOCommon.lib;%(AdditionalDependencies) - ..\..\MgDev\Oem\Fdo\Lib64;%(AdditionalLibraryDirectories) + ..\..\MgDev\Oem\Fdo\Lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true Console true From svn_mapguide at osgeo.org Mon Nov 2 16:11:29 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 16:11:29 -0800 Subject: [mapguide-commits] r10245 - trunk/Installer/FdoRegUtil Message-ID: <20261103001129.6E1A41A3B74@trac.osgeo.org> Author: jng Date: 2026-11-02 16:11:28 -0800 (Mon, 02 Nov 2026) New Revision: 10245 Removed: trunk/Installer/FdoRegUtil/FdoRegUtil.vcproj Log: Remove old and unused project Deleted: trunk/Installer/FdoRegUtil/FdoRegUtil.vcproj =================================================================== --- trunk/Installer/FdoRegUtil/FdoRegUtil.vcproj 2026-11-03 00:10:38 UTC (rev 10244) +++ trunk/Installer/FdoRegUtil/FdoRegUtil.vcproj 2026-11-03 00:11:28 UTC (rev 10245) @@ -1,403 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From svn_mapguide at osgeo.org Mon Nov 2 16:30:54 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 16:30:54 -0800 Subject: [mapguide-commits] r10246 - in branches/4.0/MgDev/Bindings/src/Bindings/DotNet: Foundation Geometry MapGuideCommon PlatformBase Portable Web Message-ID: <20261103003054.EE6111A45F5@trac.osgeo.org> Author: jng Date: 2026-11-02 16:30:53 -0800 (Mon, 02 Nov 2026) New Revision: 10246 Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt Log: #2493: Use same SWIG flags for dotnet bindings on Linux as Windows Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Foundation/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -33,13 +33,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${FoundationUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -69,6 +69,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module FoundationUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Geometry/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -37,13 +37,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${GeometryUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -73,6 +73,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module GeometryUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/MapGuideCommon/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -34,13 +34,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${MapGuideCommonUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -74,6 +74,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module MapGuideCommonUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/PlatformBase/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -34,13 +34,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${PlatformBaseUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -70,6 +70,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module PlatformBaseUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -31,13 +31,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${PortableUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -71,6 +71,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module PortableUnmanagedApi Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-03 00:11:28 UTC (rev 10245) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Web/CMakeLists.txt 2026-11-03 00:30:53 UTC (rev 10246) @@ -37,13 +37,13 @@ endif (WIN32) message(STATUS "Using IMake at: ${IMAKE_TOOL}") -# SWIG_CSHARP_NO_WSTRING_HELPER is deliberately absent: SWIG 4.3 still emits the -# references to SWIGWStringHelper while no longer emitting the class itself, so the -# generated code does not compile (CS0103). Letting the helper be generated is -# correct on both 4.0 and 4.3 - each module is generated into its own assembly, so -# there is nothing for it to collide with. The other two suppressions are honoured -# consistently and stay. The Windows build sets all three from *.vcxproj and does not -# use this file. +# The SWIG suppressions are the same set the Windows project (*.vcxproj) passes, and they have +# to stay in step: both platforms generate the C# with SWIG 4.3.1 (here from the build +# cmake_bootstrap.sh makes, on Windows from the 4.3.1 binary install $(SWIG_DIR) points at), so +# the flags are what decide whether the two produce the same C#. SWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER +# is required, not optional: 4.3 no longer emits the SWIGWStringHelper class, and without that +# flag the exception helper still references it, so the generated code does not compile (CS0103). +# Suppressing the class instead of its remaining user is the mistake to avoid. add_custom_command(OUTPUT ${WebUnmanagedApi_SRCS} COMMAND ${CMAKE_COMMAND} -E echo Removing: ${MG_DOTNET_PROJECT_DIR}/*.cs COMMAND ${CMAKE_COMMAND} -E remove -f @@ -73,6 +73,8 @@ COMMAND ${SWIG_EXECUTABLE} -c++ -csharp ${SWIG_DOTNET_DEFS} -DDOTNETCORE -DSWIG_CSHARP_NO_EXCEPTION_HELPER -DSWIG_CSHARP_NO_STRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_HELPER + -DSWIG_CSHARP_NO_WSTRING_EXCEPTION_HELPER -namespace OSGeo.MapGuide -nodefaultctor -nodefaultdtor -module WebUnmanagedApi From svn_mapguide at osgeo.org Mon Nov 2 17:40:23 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 17:40:23 -0800 Subject: [mapguide-commits] r10247 - branches/4.0/MgDev Message-ID: <20261103014023.3D69D1B80B8@trac.osgeo.org> Author: jng Date: 2026-11-02 17:40:22 -0800 (Mon, 02 Nov 2026) New Revision: 10247 Modified: branches/4.0/MgDev/cmake_linuxapt.sh Log: Rework cmake_linuxapt.sh for more component flexibility Modified: branches/4.0/MgDev/cmake_linuxapt.sh =================================================================== --- branches/4.0/MgDev/cmake_linuxapt.sh 2026-11-03 00:30:53 UTC (rev 10246) +++ branches/4.0/MgDev/cmake_linuxapt.sh 2026-11-03 01:40:22 UTC (rev 10247) @@ -26,6 +26,7 @@ USE_CCACHE=0 USE_STATIC_OPENSSL=0 USE_PHP_SHARED_MBSTRING=0 +PHP_ONLY=0 while [ $# -gt 0 ]; do # Until you run out of parameters... case "$1" in --prefix) @@ -46,6 +47,9 @@ --with-php-shared-mbstring) USE_PHP_SHARED_MBSTRING=1 ;; + --php-only) + PHP_ONLY=1 + ;; # "infecting" our ccache with root-elevated cached object files is probably a bad look # especially if we need to non-elevated ccache builds further down the road #--with-ccache) @@ -59,6 +63,7 @@ echo " --working-dir [working directory, default: ~/mapguide_linuxapt_build]" echo " --with-static-openssl [use static openssl]" echo " --with-php-shared-mbstring [build PHP mbstring extension as a shared module]" + echo " --php-only [build and install only the PHP interpreter, then exit]" echo " --help [Display usage]" #echo " --with-ccache [Use ccache]" exit @@ -169,6 +174,86 @@ echo "Has PHP: ${HAS_PHP}" echo "Has Tomcat Connector: ${HAS_TOMCAT_CONNECTOR}" +# Builds and installs the PHP interpreter. Split out so that --php-only can run just this, for a +# dev loop that needs an interpreter for the binding test suite without the rest of the web tier. +build_php() +{ + # The configure flavour has to be decided before the reuse test below, which compares it with + # what an existing tree was configured with. + PHP_APXS_OPT= + if [ -x "${MG_INSTALL_WEB_PREFIX}/apache2/bin/apxs" ]; then + PHP_APXS_OPT="--with-apxs2=${MG_INSTALL_WEB_PREFIX}/apache2/bin/apxs" + fi + # The MapGuide extension is built against a thread-safe PHP, because the web tier it ships with + # runs Apache's threaded MPM. --with-apxs2 already implies that; a CLI-only PHP has to ask for + # it, or the extension refuses to load with "Module compiled with build ID=API...,TS / PHP + # compiled with build ID=API...,NTS". + PHP_ZTS_OPT= + if [ -z "$PHP_APXS_OPT" ]; then + PHP_ZTS_OPT="--enable-zts" + fi + if test $USE_PHP_SHARED_MBSTRING -eq 1; then + PHP_MBSTRING_OPT="--enable-mbstring=shared" + else + PHP_MBSTRING_OPT="--enable-mbstring" + fi + + PHP_NEEDS_BUILD=0 + if test $HAS_PHP -eq 0; then + PHP_NEEDS_BUILD=1 + elif [ -f "${PHP_WORKDIR}/config.nice" ]; then + # An extracted tree is only reusable when it was configured the same way. A tree configured + # with --with-apxs2 cannot even `make install` while httpd's headers are absent (install + # rebuilds sapi/apache2handler), and one configured without it is an NTS PHP that the TS + # extension will not load. + if [ -n "$PHP_APXS_OPT" ] && ! grep -q -- "--with-apxs2" "${PHP_WORKDIR}/config.nice"; then + echo "PHP is configured without --with-apxs2 but httpd is installed; rebuilding with it" + PHP_NEEDS_BUILD=1 + fi + if [ -z "$PHP_APXS_OPT" ] && grep -q -- "--with-apxs2" "${PHP_WORKDIR}/config.nice"; then + echo "PHP is configured with --with-apxs2 but httpd is not installed; rebuilding CLI-only" + PHP_NEEDS_BUILD=1 + fi + if [ -n "$PHP_ZTS_OPT" ] && ! grep -q -- "--enable-zts" "${PHP_WORKDIR}/config.nice"; then + echo "PHP is configured without thread safety; rebuilding with --enable-zts" + PHP_NEEDS_BUILD=1 + fi + fi + if [ -z "$PHP_APXS_OPT" ] && test $PHP_NEEDS_BUILD -eq 1; then + echo "No apxs at ${MG_INSTALL_WEB_PREFIX}/apache2/bin/apxs: building PHP without the Apache module" + fi + + if test $PHP_NEEDS_BUILD -eq 1; then + # Reconfigure from clean sources rather than over a half-built tree with stale objects + if [ -d "${PHP_WORKDIR}" ] && [ -f "${PHP_WORKDIR}/config.nice" ]; then + rm -rf "${PHP_WORKDIR}" + fi + cd "$SOURCE_DIR/Oem/LinuxApt" || exit + if [ ! -d "${PHP_WORKDIR}" ]; then + echo "Extracting PHP tarball" + tar -jxf php-${PHP_VER}.tar.bz2 -C "${LA_WORKDIR}" + check_build + fi + echo "Configuring PHP" + cd "${PHP_WORKDIR}" || exit + ./configure --prefix="${MG_INSTALL_WEB_PREFIX}/php" $PHP_APXS_OPT $PHP_ZTS_OPT --with-openssl $PHP_MBSTRING_OPT --enable-gd --with-jpeg --with-libxml 2>&1 | tee "$LINUXAPT_WORK_DIR/configure_php.log" + check_build + echo "Building PHP. Please wait" + make 2>&1 | tee "$LINUXAPT_WORK_DIR/make_php.log" + check_build + fi + + cd "${PHP_WORKDIR}" || exit + echo "Installing PHP" + make install 2>&1 | tee "$LINUXAPT_WORK_DIR/make_install_php.log" + check_build +} + +if test $PHP_ONLY -eq 1; then + build_php + exit 0 +fi + if test $HAS_HTTPD -eq 0; then cd "$SOURCE_DIR/Oem/LinuxApt" || exit echo "Extracting httpd tarballs" @@ -214,30 +299,8 @@ export MENTOR_DICTIONARY_PATH=${MG_INST_PREFIX}/share/gis/coordsys END-OF-ENVVARS -if test $HAS_PHP -eq 0; then - cd "$SOURCE_DIR/Oem/LinuxApt" || exit - echo "Extracting PHP tarball" - tar -jxf php-${PHP_VER}.tar.bz2 -C "${LA_WORKDIR}" - check_build - echo "Configuring PHP" - cd "${PHP_WORKDIR}" || exit - if test $USE_PHP_SHARED_MBSTRING -eq 1; then - ./configure --prefix="${MG_INSTALL_WEB_PREFIX}/php" --with-apxs2="${MG_INSTALL_WEB_PREFIX}/apache2/bin/apxs" --with-openssl --enable-mbstring=shared --enable-gd --with-jpeg --with-libxml 2>&1 | tee "$LINUXAPT_WORK_DIR/configure_php.log" - check_build - else - ./configure --prefix="${MG_INSTALL_WEB_PREFIX}/php" --with-apxs2="${MG_INSTALL_WEB_PREFIX}/apache2/bin/apxs" --with-openssl --enable-mbstring --enable-gd --with-jpeg --with-libxml 2>&1 | tee "$LINUXAPT_WORK_DIR/configure_php.log" - check_build - fi - echo "Building PHP. Please wait" - make 2>&1 | tee "$LINUXAPT_WORK_DIR/make_php.log" - check_build -fi +build_php -cd "${PHP_WORKDIR}" || exit -echo "Installing PHP" -make install 2>&1 | tee "$LINUXAPT_WORK_DIR/make_install_php.log" -check_build - if test $HAS_TOMCAT_CONNECTOR -eq 0; then cd "$SOURCE_DIR/Oem/LinuxApt" || exit echo "Extracting Tomcat Connector tarball" From svn_mapguide at osgeo.org Mon Nov 2 17:57:19 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 17:57:19 -0800 Subject: [mapguide-commits] r10248 - branches/4.0/MgDev/Bindings/src/Test/Php Message-ID: <20261103015720.1482E1B9117@trac.osgeo.org> Author: jng Date: 2026-11-02 17:57:18 -0800 (Mon, 02 Nov 2026) New Revision: 10248 Modified: branches/4.0/MgDev/Bindings/src/Test/Php/Validate.php branches/4.0/MgDev/Bindings/src/Test/Php/ValidateUtils.php Log: #2494: Fix DWF content comparison on Linux Modified: branches/4.0/MgDev/Bindings/src/Test/Php/Validate.php =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Php/Validate.php 2026-11-03 01:40:22 UTC (rev 10247) +++ branches/4.0/MgDev/Bindings/src/Test/Php/Validate.php 2026-11-03 01:57:18 UTC (rev 10248) @@ -140,6 +140,9 @@ if ($_POST['testExecutionMode'] == "validate") { + //A stored drawing baseline may predate the current DWF normalization, so apply it here too + $expectedResult = ValidateUtils::NormalizeDwfResult($operation, $expectedResult); + //Normalize line endings so comparisons don't fall over due to incorrect line endings when expected results were entered in SQLite GUI $normResultData = str_replace("\r\n", "\n", $resultData); $normExpectedResult = str_replace("\r\n", "\n", $expectedResult); @@ -343,6 +346,9 @@ if ($_POST['testExecutionMode'] == "validate") { + //A stored drawing baseline may predate the current DWF normalization, so apply it here too + $expectedResult = ValidateUtils::NormalizeDwfResult($operation, $expectedResult); + //Normalize line endings. Don't want stray CRLFs derailing verification $resultData = str_replace("\r\n", "\n", $resultData); $expectedResult = str_replace("\r\n", "\n", $expectedResult); Modified: branches/4.0/MgDev/Bindings/src/Test/Php/ValidateUtils.php =================================================================== --- branches/4.0/MgDev/Bindings/src/Test/Php/ValidateUtils.php 2026-11-03 01:40:22 UTC (rev 10247) +++ branches/4.0/MgDev/Bindings/src/Test/Php/ValidateUtils.php 2026-11-03 01:57:18 UTC (rev 10248) @@ -134,7 +134,34 @@ public static function RemoveDwfSectionName($actualResult) { - $newResult = substr($actualResult, strpos($actualResult, ".w2d")); + //A drawing operation returns a DWF package, which is a zip: the section entry's name ends in + //".w2d" and what follows is that entry's raw deflate data. When the data was written as a + //single stored block its header carries the uncompressed size and the one's complement of + //that size, so the packaging rather than the section would be compared. The W2D end marker + //is written with WD_NEWLINE, which is "\r\n" only when built with MSVC, so the same section + //is one byte shorter on Linux and the size fields of every stored block differ. Keep the + //stream payload only; the result is already in this form when it comes back from a baseline. + $start = strpos($actualResult, ".w2d"); + if (false === $start) + { + return $actualResult; + } + + $newResult = substr($actualResult, $start + strlen(".w2d")); + if (strlen($newResult) >= 5) + { + $blockHeader = ord($newResult[0]); + $blockLength = ord($newResult[1]) | (ord($newResult[2]) << 8); + $blockNotLength = ord($newResult[3]) | (ord($newResult[4]) << 8); + + //BFINAL set and BTYPE == 00 (stored) with LEN and NLEN consistent: the header is 1 byte + //plus the two sizes, so decode the block by dropping it + if ((0x01 == ($blockHeader & 0x07)) && (0xFFFF == (($blockLength ^ $blockNotLength) & 0xFFFF))) + { + $newResult = substr($newResult, 5); + } + } + if (0!=strpos($newResult, "EndOfDWF")) { $newResult = substr($newResult, 0, strpos($newResult, "EndOfDWF")); @@ -142,6 +169,18 @@ return $newResult; } + //A drawing result is normalized before a baseline is stored, but a baseline written by an older + //version of this script still carries the DWF zip framing, so the normalization is applied to a + //stored result as well. It is idempotent: an already normalized result has no ".w2d" to find. + public static function NormalizeDwfResult($operation, $resultData) + { + if (("GETDRAWINGLAYER" == $operation) || ("GETDRAWINGSECTION" == $operation)) + { + return self::RemoveDwfSectionName($resultData); + } + return $resultData; + } + public static function StripFdoVersionFromProvider($actualResult) { $start = strpos($actualResult, ""); From svn_mapguide at osgeo.org Mon Nov 2 18:14:38 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 18:14:38 -0800 Subject: [mapguide-commits] r10249 - in branches/4.0/MgDev: Common/Renderers Server/src/UnitTesting Message-ID: <20261103021438.6524D1BA162@trac.osgeo.org> Author: jng Date: 2026-11-02 18:14:38 -0800 (Mon, 02 Nov 2026) New Revision: 10249 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp branches/4.0/MgDev/Common/Renderers/KmlRenderer.h branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp Log: Fix potential duplicate KML placemark names. Fixes #2762 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 01:57:18 UTC (rev 10248) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 02:14:38 UTC (rev 10249) @@ -50,7 +50,8 @@ m_elevation(0.0), m_extrude(false), m_elevType(RS_ElevationType_RelativeToGround), - m_metersPerUnit(metersPerUnit) + m_metersPerUnit(metersPerUnit), + m_placemarkName(L"") { m_kmlContent = m_mainContent; m_pixelSize = METERS_PER_INCH / dpi; @@ -102,7 +103,7 @@ if (m_featureCount > 0) { //end last feature - m_kmlContent->WriteString(""); + EndPlacemark(); } m_kmlContent = m_mainContent; @@ -125,6 +126,21 @@ } +void KmlRenderer::EndPlacemark() +{ + //a placemark has one name at most, and an empty one shows as a blank entry + //next to the real name in Google Earth, so only write it when there is one + if (!m_placemarkName.empty()) + { + m_kmlContent->WriteString("WriteString(m_placemarkName, false); + m_kmlContent->WriteString("]]>"); + m_placemarkName.clear(); + } + m_kmlContent->WriteString(""); +} + + void KmlRenderer::StartFeature(RS_FeatureReader* /*feature*/, bool /*initialPass*/, const RS_String* tooltip, @@ -137,7 +153,7 @@ if (m_featureCount > 0) { //end last feature - m_kmlContent->WriteString(""); + EndPlacemark(); } if (theme == NULL || theme->empty()) { @@ -156,6 +172,7 @@ m_themeMap[*theme] = m_kmlContent; } } + m_placemarkName.clear(); m_kmlContent->WriteString(""); if ((tooltip != NULL && tooltip->length() > 0) || (url != NULL && url->length() > 0)) @@ -380,9 +397,12 @@ } char buffer[256]; - m_kmlContent->WriteString("WriteString(mdef.name(), false); - m_kmlContent->WriteString("]]>"); + //the marker's name is its symbol name, not the feature's, so it is only used + //when nothing else names the placemark + if (m_placemarkName.empty()) + { + m_placemarkName = mdef.name(); + } m_kmlContent->WriteString(""); WriteElevationSettings(); m_kmlContent->WriteString(""); @@ -402,9 +422,10 @@ double /*scaleLimit*/) { // char buffer[256]; - m_kmlContent->WriteString("WriteString(text, false); - m_kmlContent->WriteString("]]>"); + if (!text.empty()) + { + m_placemarkName = text; + } /* m_kmlContent->WriteString(""); for (int i = 0; i < nlabels; i++) { Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 01:57:18 UTC (rev 10248) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 02:14:38 UTC (rev 10249) @@ -166,6 +166,7 @@ KmlRenderer& operator=(const KmlRenderer&); void ProcessOneMarker(double x, double y, RS_MarkerDef& mdef, bool allowOverpost); + void EndPlacemark(); void WriteStyle(RS_FillStyle& fill); void WriteStyle(RS_LineStroke& lsym); void WriteStyle(double scale, const std::wstring& href); @@ -198,6 +199,9 @@ bool m_extrude; double m_metersPerUnit; RS_ElevationType m_elevType; + // name to write for the placemark being rendered, collected from its label + // (or, failing that, from its marker) and written when the placemark closes + RS_String m_placemarkName; }; #endif Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-11-03 01:57:18 UTC (rev 10248) +++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-11-03 02:14:38 UTC (rev 10249) @@ -56,6 +56,7 @@ target_link_libraries(MgUnitTesting ${CPPUNIT_LIBRARIES} + MgRenderers${MG_VERSION_SUFFIX} MgStylization${MG_VERSION_SUFFIX} MgMdfModel${MG_VERSION_SUFFIX} ) \ No newline at end of file Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 01:57:18 UTC (rev 10248) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 02:14:38 UTC (rev 10249) @@ -20,9 +20,13 @@ #include "ServerResourceService.h" #include "ServerSiteService.h" #include "FoundationDefs.h" +#include "KmlRenderer.h" #include "TestServiceFactory.h" #include "catch.hpp" +#include +#include + static STRING RemoveSessionIds(CREFSTRING content) { STRING newContent = content; @@ -349,4 +353,134 @@ { throw; } -} \ No newline at end of file +} + +////////////////////////////////////////////////////////////////////////////// +// Kml renderer placemark naming (ticket #2762) +// +// These drive the renderer directly, in the order PointAdapter does for a point +// feature that has both a point symbol and a label: ProcessMarker() and then +// ProcessLabelGroup(). A placemark must carry at most one , and never an +// empty one, or Google Earth shows a blank name next to the real one. + +namespace +{ + // Splits a KML fragment into its elements + std::vector GetPlacemarks(const std::string& kml) + { + static const std::string startTag(""); + static const std::string endTag(""); + + std::vector placemarks; + std::string::size_type start = kml.find(startTag); + while (start != std::string::npos) + { + std::string::size_type end = kml.find(endTag, start); + if (end == std::string::npos) + break; + + end += endTag.length(); + placemarks.push_back(kml.substr(start, end - start)); + start = kml.find(startTag, end); + } + return placemarks; + } + + int CountOccurrences(const std::string& text, const std::string& what) + { + int count = 0; + std::string::size_type pos = text.find(what); + while (pos != std::string::npos) + { + ++count; + pos = text.find(what, pos + what.length()); + } + return count; + } + + // Renders one point feature with a point symbol of the given marker name and, + // when label is not empty, with a label - this is what DefaultStylizer does + // for a point layer. The resulting KML is returned. + std::string RenderPointFeature(CREFSTRING markerName, CREFSTRING label) + { + KmlContent kmlContent; + RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); + + RS_UIGraphic graphic(NULL, 0, L""); + RS_LayerUIInfo layerInfo(L"Parcels", L"Library://UnitTests/Layers/Parcels.LayerDefinition", + L"", true, true, false, L"", L"", true, true, 0.0, graphic); + RS_FeatureClassInfo classInfo(L"Parcels", L"Library://UnitTests/Data/Parcels.FeatureSource", L"Geometry"); + renderer.StartLayer(&layerInfo, &classInfo); + + LineBuffer lb(4); + lb.MoveTo(-87.7, 43.75); + + renderer.StartFeature(NULL, true, NULL, NULL, NULL, 0.0, 0.0, RS_ElevationType_RelativeToGround); + + RS_MarkerDef mdef; + mdef.name() = markerName; + renderer.ProcessMarker(&lb, mdef, true, NULL); + + if (!label.empty()) + renderer.ProcessLabelGroup(NULL, 0, label, RS_OverpostType_FirstFit, true, &lb, 0.0); + + renderer.EndLayer(); + return kmlContent.GetString(); + } +} + +TEST_CASE("KmlPlacemarkNameNotDuplicated", "[KmlService]") +{ + std::string kml = RenderPointFeature(L"", L"PARCEL 123"); + + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 1); + + std::string placemark = placemarks.front(); + INFO(placemark); + REQUIRE(placemark.find("") != std::string::npos); + REQUIRE(CountOccurrences(placemark, "") == 1); +} + +TEST_CASE("KmlPlacemarkOmitsEmptyName", "[KmlService]") +{ + // a point symbol whose marker has no name, and no label + std::string kml = RenderPointFeature(L"", L""); + + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 1); + + std::string placemark = placemarks.front(); + INFO(placemark); + REQUIRE(CountOccurrences(placemark, "") == 0); +} + +TEST_CASE("KmlPlacemarkLabelWinsOverMarkerName", "[KmlService]") +{ + // the marker name belongs to the symbol, not to the feature, so the label + // is the placemark name when both are present + std::string kml = RenderPointFeature(L"BlockSymbol", L"PARCEL 123"); + + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 1); + + std::string placemark = placemarks.front(); + INFO(placemark); + REQUIRE(CountOccurrences(placemark, "") == 1); + REQUIRE(placemark.find("") != std::string::npos); +} + +TEST_CASE("KmlPlacemarkMarkerNameWithNoLabel", "[KmlService]") +{ + // with no label, the marker name is all there is to name the placemark with + std::string kml = RenderPointFeature(L"BlockSymbol", L""); + + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 1); + + std::string placemark = placemarks.front(); + INFO(placemark); + REQUIRE(CountOccurrences(placemark, "") == 1); + REQUIRE(placemark.find("") != std::string::npos); +} From svn_mapguide at osgeo.org Mon Nov 2 19:52:02 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 19:52:02 -0800 Subject: [mapguide-commits] r10250 - branches/4.0/MgDev/Common/Renderers Message-ID: <20261103035202.3DA061BE98F@trac.osgeo.org> Author: jng Date: 2026-11-02 19:52:01 -0800 (Mon, 02 Nov 2026) New Revision: 10250 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h Log: #2762: Fix windows linkage error Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 02:14:38 UTC (rev 10249) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 03:52:01 UTC (rev 10250) @@ -82,9 +82,9 @@ RS_Bounds& extents, TransformMesh* xformMesh = NULL); - virtual void ProcessMarker(LineBuffer* lb, RS_MarkerDef& mdef, bool allowOverpost, RS_Bounds* bounds = NULL); + RENDERERS_API virtual void ProcessMarker(LineBuffer* lb, RS_MarkerDef& mdef, bool allowOverpost, RS_Bounds* bounds = NULL); - virtual void ProcessLabelGroup(RS_LabelInfo* labels, + RENDERERS_API virtual void ProcessLabelGroup(RS_LabelInfo* labels, int nlabels, const RS_String& text, RS_OverpostType type, From svn_mapguide at osgeo.org Mon Nov 2 20:01:25 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 20:01:25 -0800 Subject: [mapguide-commits] r10251 - in branches/4.0/MgDev/Server/src: Gws/GwsQueryEngine Gws/GwsQueryEngine/inc UnitTesting Message-ID: <20261103040125.6A2D51BEAE8@trac.osgeo.org> 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(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 ///////////////////////////////////////////////////////////////////// // @@ -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* 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(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 key = m_joinkeys.GetItem(m_joinKeyIndex); + if (key == NULL || key->IsNull()) + return false; + + const int count = static_cast(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 descriptor; + DescribeFeature(&descriptor); + + FdoPtr names = descriptor->PropertyNames(); + if (index < 0 || index >= names->GetCount()) + return NULL; + + m_indexPropertyName = names->GetString(index); + return const_cast(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 dataValuePrimary = (FdoDataValue*)m_joinkeys.GetItem(m_joinKeyIndex); - FdoPtr 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 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 extFeatDsc; - m_prepquery->DescribeResults((IGWSExtendedFeatureDescription**)&extFeatDsc); + FdoPtr descriptor; + DescribeFeature(&descriptor); + return descriptor->ClassDefinition(); + } - classDef = extFeatDsc->ClassDefinition(); - } - } - else + if(m_usepool) { - classDef = CGwsFeatureIterator::GetClassDefinition(); + FdoPtr 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 m_blockKeys; + std::vector 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 +#include +#include + +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(IntValue)); + case FdoDataType_Int64: + return FdoDataValue::Create(static_cast(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 Columns; + std::vector > 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(i); + } + return -1; + } + }; + + static const FakeValue* FindValue(const FakeTable& table, const std::vector& row, + FdoString* name) + { + if (name == NULL) + return NULL; + + int index = table.ColumnIndex(name); + if (index < 0 || index >= static_cast(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(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& row, FdoFilter* filter); + + static bool EvaluateComparison(const FakeValue& value, FdoDataValue* right, + FdoComparisonOperations operation, bool caseInsensitive) + { + FdoPtr 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(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& row, FdoFilter* filter) + { + if (filter == NULL) + return true; + + FdoBinaryLogicalOperator* binary = dynamic_cast(filter); + if (binary != NULL) + { + FdoPtr leftOperand = binary->GetLeftOperand(); + FdoPtr 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(filter); + if (comparison != NULL) + { + FdoPtr leftExpression = comparison->GetLeftExpression(); + FdoPtr rightExpression = comparison->GetRightExpression(); + + FdoIdentifier* identifier = dynamic_cast(leftExpression.p); + FdoDataValue* rightValue = dynamic_cast(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(filter); + if (inCondition != NULL) + { + FdoPtr identifier = inCondition->GetPropertyName(); + FdoPtr values = inCondition->GetValues(); + if (identifier == NULL || values == NULL) + return true; + + const FakeValue* value = FindValue(table, row, identifier->GetName()); + if (value == NULL) + return false; + + FdoPtr left = value->CreateDataValue(); + for (FdoInt32 i = 0; i < values->GetCount(); ++i) + { + FdoPtr candidate = values->GetItem(i); + FdoDataValue* right = dynamic_cast(candidate.p); + if (right == NULL || left->IsNull() || right->IsNull()) + continue; + + FdoStringValue* leftString = dynamic_cast(left.p); + FdoStringValue* rightString = dynamic_cast(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 BuildDescriptor(const FakeTable& table) + { + FdoPtr classDef = FdoFeatureClass::Create(); + classDef->SetName(table.ClassName.c_str()); + + FdoPtr properties = classDef->GetProperties(); + FdoPtr identity = classDef->GetIdentityProperties(); + FdoPtr selectList = FdoIdentifierCollection::Create(); + + for (size_t i = 0; i < table.Columns.size(); ++i) + { + const FakeColumn& column = table.Columns[i]; + + FdoPtr 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 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(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(i)); + } + } + + virtual ~FakeFeatureIterator() + { + if (m_filter != NULL) + m_filter->Release(); + m_descriptor->Release(); + } + + virtual bool ReadNext() + { + ++m_position; + return m_position < static_cast(m_rows.size()); + } + + virtual void Close() + { + m_position = static_cast(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 values = CGwsDataValueCollection::Create(); + for (FdoInt32 i = 0; propertyNames != NULL && i < propertyNames->GetCount(); ++i) + { + FdoPtr 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(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(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(value->IntValue); + } + + virtual FdoInt16 GetInt16(FdoString* propertyName) + { + return static_cast(GetInt64(propertyName)); + } + + virtual FdoInt32 GetInt32(FdoString* propertyName) + { + return static_cast(GetInt64(propertyName)); + } + + virtual FdoInt64 GetInt64(FdoString* propertyName) + { + const FakeValue* value = CurrentValue(propertyName); + if (value == NULL) + return 0; + return value->Type == FdoDataType_Double ? static_cast(value->DoubleValue) + : value->IntValue; + } + + virtual float GetSingle(FdoString* propertyName) + { + return static_cast(GetDouble(propertyName)); + } + + virtual FdoInt32 GetRevisionNumber() + { + return -1; + } + + virtual FdoInt32 GetCacheId() + { + return 0; + } + + virtual GWSFeatureId GetFeatureId() + { + FdoPtr values = CGwsDataValueCollection::Create(); + for (size_t i = 0; i < m_table->Columns.size(); ++i) + { + if (m_table->Columns[i].IsIdentity) + { + FdoPtr 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(m_table->Columns.size())) + return NULL; + return const_cast(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(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 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 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 "=" strings. + // A left row with no matching right row is reported as "=NULL". + static std::vector RunJoin(const FakeTable& leftTable, const FakeTable& rightTable, + const std::vector& leftJoinProperties, + const std::vector& rightJoinProperties, + EGwsJoinMethod method, EGwsQueryType joinType, + bool forceOneToOne = true, + int* rightExecuteCount = NULL) + { + FdoPtr leftDescriptor = BuildDescriptor(leftTable); + FdoPtr 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 leftColumns = FdoStringCollection::Create(); + for (size_t i = 0; i < leftJoinProperties.size(); ++i) + leftColumns->Add(leftJoinProperties[i].c_str()); + FdoPtr rightColumns = FdoStringCollection::Create(); + for (size_t i = 0; i < rightJoinProperties.size(); ++i) + rightColumns->Add(rightJoinProperties[i].c_str()); + + FdoPtr leftDefinition = IGWSFeatureQueryDefinition::Create( + NULL, GWSQualifiedName(L"TestSchema", leftTable.ClassName.c_str()), NULL); + FdoPtr rightDefinition = IGWSFeatureQueryDefinition::Create( + NULL, GWSQualifiedName(L"TestSchema", rightTable.ClassName.c_str()), NULL); + + FdoPtr 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 sourceQuery = + static_cast( + CGwsFeatureSourceQuery::CreateInstance(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 rows; + + { + FdoPtr 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 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 RunJoin(const FakeTable& leftTable, const FakeTable& rightTable, + const wchar_t* leftJoinProperty, + const wchar_t* rightJoinProperty, + EGwsJoinMethod method, EGwsQueryType joinType, + bool forceOneToOne = true) + { + std::vector leftProperties(1, leftJoinProperty); + std::vector 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""; + } + } + + static const EGwsJoinMethod kImplementedJoinMethods[] = + { + eGwsSortMerge, + eGwsNestedLoops, + eGwsNestedLoopSortedBlock, + eGwsBatchSortedBlock + }; + + static std::string Describe(const std::vector& 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 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 >& 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 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 >& 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& columns, + const std::vector >& rows) + { + FakeTable table; + table.ClassName = className; + table.Columns = columns; + table.Rows = rows; + return table; + } + + static FakeTable MakeStringTable(const wchar_t* className, + const std::vector >& 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 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 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 > leftRows; + std::vector > 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 > leftRows; + std::vector > 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 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 @@ Disabled - ..\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) + ..\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) WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) Async EnableFastChecks @@ -120,7 +120,7 @@ Disabled - ..\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) + ..\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) WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) Async EnableFastChecks @@ -146,7 +146,7 @@ MaxSpeed - ..\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) + ..\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) WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) Async MultiThreadedDLL @@ -173,7 +173,7 @@ MaxSpeed - ..\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) + ..\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) WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) Async MultiThreadedDLL @@ -208,6 +208,7 @@ + @@ -235,6 +236,14 @@ + + {DB2CC2E3-4754-4932-B865-424A44E58FD0} + false + + + {8D947123-A12C-45DA-BD00-8B62DD5A0459} + false + {a82adc7d-4da4-42f2-9bf6-df5dcfb44425} false From svn_mapguide at osgeo.org Mon Nov 2 20:05:51 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 20:05:51 -0800 Subject: [mapguide-commits] r10252 - in branches/4.0/MgDev: Common/Foundation/Exception Common/Foundation/System Server/src/UnitTesting Message-ID: <20261103040551.689EF1BF21F@trac.osgeo.org> Author: jng Date: 2026-11-02 20:05:50 -0800 (Mon, 02 Nov 2026) New Revision: 10252 Modified: branches/4.0/MgDev/Common/Foundation/Exception/Exception.cpp branches/4.0/MgDev/Common/Foundation/System/Resources.cpp branches/4.0/MgDev/Common/Foundation/System/Resources.h branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp Log: Explain localized exception message formatting failures Preserve the exception raised during message lookup or formatting and include a diagnostic in the fallback message. Report missing resource details or required and supplied argument counts, along with the original exception's throw site. Factor placeholder counting into MgResources and cover both failure cases in the Misc test scope. Verified with Release and Debug server builds and Misc tests (17 test cases, 218 assertions). The Debug CRT leak report matched the baseline. Fixes #2867 Modified: branches/4.0/MgDev/Common/Foundation/Exception/Exception.cpp =================================================================== --- branches/4.0/MgDev/Common/Foundation/Exception/Exception.cpp 2026-11-03 04:01:24 UTC (rev 10251) +++ branches/4.0/MgDev/Common/Foundation/Exception/Exception.cpp 2026-11-03 04:05:50 UTC (rev 10252) @@ -271,6 +271,94 @@ /////////////////////////////////////////////////////////////////////////////// /// \brief +/// Format the location at which an exception was thrown. +/// +/// The first frame recorded on an exception is the one passed to its +/// constructor, so it names the method and line which threw it. +/// +static STRING FormatThrownAt(const MgStringCollection& methodNames, + const MgStringCollection& lineNumbers, const MgStringCollection& fileNames) +{ + STRING location; + + if (methodNames.GetCount() > 0 && lineNumbers.GetCount() > 0 && fileNames.GetCount() > 0) + { + location = methodNames.GetItem(0); + location += L" ("; + location += fileNames.GetItem(0); + location += L":"; + location += lineNumbers.GetItem(0); + location += L")"; + } + + return location; +} + +/////////////////////////////////////////////////////////////////////////////// +/// \brief +/// Format the reason why a message resource could not be formatted. +/// +/// This is appended to the "failed to retrieve message" fallback, so that a +/// message which cannot be formatted explains itself instead of having its +/// reason discarded. +/// +/// No message resource is formatted here, because the caller has already +/// failed to do that. Everything reported comes from the failed lookup and +/// from the exceptions involved, so this cannot fail and cannot recurse back +/// into the formatter it is reporting on. +/// +static STRING FormatMessageFailure(CREFSTRING locale, CREFSTRING section, + CREFSTRING resourceId, MgStringCollection* arguments, CREFSTRING resourceStr, + CREFSTRING cause, CREFSTRING thrownAt) +{ + STRING reason; + + if (resourceStr.empty()) + { + reason = L"The message resource \""; + reason += resourceId; + reason += L"\" was not found in section \""; + reason += section; + reason += L"\" for locale \""; + reason += locale; + reason += L"\"."; + } + else + { + STRING required; + STRING supplied; + + MgUtil::Int32ToString(MgResources::GetRequiredArgumentCount(resourceStr), required); + MgUtil::Int32ToString(NULL != arguments ? arguments->GetCount() : 0, supplied); + + reason = L"The message resource \""; + reason += resourceId; + reason += L"\" requires "; + reason += required; + reason += L" argument(s) but "; + reason += supplied; + reason += L" were supplied."; + } + + if (!cause.empty()) + { + reason += L" "; + reason += cause; + reason += L"."; + } + + if (!thrownAt.empty()) + { + reason += L" Exception thrown in "; + reason += thrownAt; + reason += L"."; + } + + return reason; +} + +/////////////////////////////////////////////////////////////////////////////// +/// \brief /// Format a message using the specified locale. /// STRING MgException::FormatMessage(CREFSTRING locale, CREFSTRING section, @@ -277,6 +365,10 @@ CREFSTRING resourceId, MgStringCollection* arguments) throw() { STRING message; + STRING failure; + STRING resourceStr; + STRING attemptedLocale = locale; + Ptr cause; MgResources* resources = NULL; MG_TRY() @@ -283,59 +375,41 @@ resources = MgResources::GetInstance(); + MG_CATCH_AND_RELEASE() + if (NULL != resources) { // try using the supplied locale - STRING resourceStr = resources->GetStringResource(locale, section, - resourceId); + MG_TRY() - message = resources->FormatMessage(resourceStr, arguments); - } + resourceStr = resources->GetStringResource(locale, section, resourceId); - MG_CATCH_AND_RELEASE() -#if _DEBUG - if (nullptr != mgException.p) - { - // TODO: Ideally we want to be able to capture this exception message and pass it on as the "reason" - // for why we're failing to retrieve the original exception message. But my initial attempts at this - // resulted in massive recursively nested error messages due to us: - // - // a) Currently being in the exception message formatter - // b) Naively trying to call GetExceptionMessage() on this exception which has a very high chance of - // initiating a long cascading recursive chain of similar errors due to argument mismatches on each - // recursive iteration - // - // This is just a variant of the "what do we do if the exception handler itself throws an exception?" - // problem with some extra recursion thrown in. - // - // In the absence of an actual solution, the next best thing is just to stick a breakpoint in this block - // and inspect the contents of the caught mgException in a debugger. - // - // Fortunately, most of the time the actual reason is we are throwing a MgException somewhere but not - // passing the correct amount of arguments for the exception message. So your breakpoint here hits in a - // debugger, you know that it's most likely because of this case. - // - // Or the alternative reason is the obvious mapguide_$locale.res string bundle was not found at the - // expected location. - } -#endif - if (message.empty() && NULL != resources && locale != MgResources::DefaultMessageLocale) - { - MG_TRY() + if (!resourceStr.empty()) + { + message = resources->FormatMessage(resourceStr, arguments); + } - // try using the default locale as a backup - STRING resourceStr = resources->GetStringResource(MgResources::DefaultMessageLocale, section, - resourceId); + MG_CATCH_AND_RELEASE() - message = resources->FormatMessage(resourceStr, arguments); + cause = mgException; - MG_CATCH_AND_RELEASE() -#if _DEBUG - if (nullptr != mgException.p) + if (message.empty() && locale != MgResources::DefaultMessageLocale) { - // TODO: See above TODO + // try using the default locale as a backup + MG_TRY() + + attemptedLocale = MgResources::DefaultMessageLocale; + resourceStr = resources->GetStringResource(attemptedLocale, section, resourceId); + + if (!resourceStr.empty()) + { + message = resources->FormatMessage(resourceStr, arguments); + } + + MG_CATCH_AND_RELEASE() + + cause = mgException; } -#endif } if (message.empty() && NULL != resources) @@ -342,6 +416,30 @@ { MG_TRY() + STRING causeDescription; + + // A message resource which was found but could not be formatted has a + // known reason, so only name the exception when the lookup itself is + // what failed. Otherwise the "cause" is the formatter rejecting the + // arguments, which the reason already reports. + if (resourceStr.empty() && NULL != cause.p) + { + causeDescription = cause->GetExceptionCode(); + + STRING causeThrownAt = FormatThrownAt(cause->m_methodNames, + cause->m_lineNumbers, cause->m_fileNames); + + if (!causeThrownAt.empty()) + { + causeDescription += L" was thrown in "; + causeDescription += causeThrownAt; + } + } + + failure = FormatMessageFailure(attemptedLocale, section, resourceId, + arguments, resourceStr, causeDescription, + FormatThrownAt(m_methodNames, m_lineNumbers, m_fileNames)); + MgStringCollection resourceArguments; resourceArguments.Add(resourceId); @@ -348,6 +446,12 @@ message = resources->FormatMessage( MgResources::FailedToRetrieveMessage, &resourceArguments); + if (!failure.empty()) + { + message += L" "; + message += failure; + } + MG_CATCH_AND_RELEASE() } Modified: branches/4.0/MgDev/Common/Foundation/System/Resources.cpp =================================================================== --- branches/4.0/MgDev/Common/Foundation/System/Resources.cpp 2026-11-03 04:01:24 UTC (rev 10251) +++ branches/4.0/MgDev/Common/Foundation/System/Resources.cpp 2026-11-03 04:05:50 UTC (rev 10252) @@ -451,6 +451,29 @@ } +INT32 MgResources::GetRequiredArgumentCount(CREFSTRING stringResource) +{ + wchar_t buff[4]; + + for (INT32 i = 9; i > 0; i--) + { + ACE_OS::itoa(i, buff, 10); + + STRING currentReplace = L"%"; + currentReplace += buff; + + // if placeholder for argument i exists then i + // is the number of arguments needed. + if (stringResource.find(currentReplace, 0) != string::npos) + { + return i; + } + } + + return 0; +} + + STRING MgResources::FormatMessage(CREFSTRING stringResource, MgStringCollection* arguments) { STRING value = stringResource; @@ -473,22 +496,8 @@ } // Determine the number of arguments needed - for (i = 9; i > 0; i--) - { - ACE_OS::itoa(i, buff, 10); - currentReplace = L"%"; - currentReplace += buff; - pos = stringResource.find(currentReplace, 0); + numArgsNeeded = GetRequiredArgumentCount(stringResource); - // if placeholder for argument i exists then i - // is the number of arguments needed. - if (pos != string::npos) - { - numArgsNeeded = i; - break; - } - } - // If the argument collection size is valid, format message if ((numArgsProvided == numArgsNeeded) && (numArgsProvided <= 9)) { Modified: branches/4.0/MgDev/Common/Foundation/System/Resources.h =================================================================== --- branches/4.0/MgDev/Common/Foundation/System/Resources.h 2026-11-03 04:01:24 UTC (rev 10251) +++ branches/4.0/MgDev/Common/Foundation/System/Resources.h 2026-11-03 04:05:50 UTC (rev 10252) @@ -125,6 +125,20 @@ STRING GetStringResource(CREFSTRING locale, CREFSTRING section, CREFSTRING resourceId); /// \brief + /// Get the number of arguments a string resource requires to be formatted. + /// + /// This is the highest argument placeholder (%1 to %9) present in the + /// string resource. A resource with no placeholders requires no arguments. + /// + /// \param stringResource + /// The STRING to be examined. ie. "%1 %2" + /// + /// \return + /// The number of arguments the string resource requires + /// + static INT32 GetRequiredArgumentCount(CREFSTRING stringResource); + + /// \brief /// Format a string resource that was retrieved using GetStringResource. The number of arguments /// needed to be replaced in the stringResource must match the size of the collection given. /// Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp 2026-11-03 04:01:24 UTC (rev 10251) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp 2026-11-03 04:05:50 UTC (rev 10252) @@ -83,6 +83,44 @@ } } +TEST_CASE("ExceptionMessageFailureReason", "[Misc]") +{ + // An exception whose message resource does not exist must say so, and must + // name the method which threw it. + { + Ptr ex = new MgException(L"MgThisExceptionCodeDoesNotExist", + L"TestMisc::TestCase_ExceptionMessageFailureReason", __LINE__, __WFILE__, + nullptr, L"", nullptr); + + STRING msg = ex->GetExceptionMessage(TestServiceFactory::TEST_LOCALE); + + REQUIRE(msg.find(L"Failed to retrieve message for \"MgThisExceptionCodeDoesNotExist\"") != STRING::npos); + REQUIRE(msg.find(L"was not found in section \"Exceptions\"") != STRING::npos); + REQUIRE(msg.find(L"TestMisc::TestCase_ExceptionMessageFailureReason") != STRING::npos); + } + + // An exception thrown with more message arguments than its message resource + // takes must report how many it takes and how many were supplied. + { + Ptr arguments = new MgStringCollection(); + arguments->Add(L"1"); + arguments->Add(L"a"); + arguments->Add(L"2"); + arguments->Add(L"b"); + + Ptr ex = new MgException(MgExceptionCodes::MgInvalidArgumentException, + L"TestMisc::TestCase_ExceptionMessageFailureReason", __LINE__, __WFILE__, + arguments, L"MgResourcesIdentical", NULL); + + STRING msg = ex->GetExceptionMessage(TestServiceFactory::TEST_LOCALE); + + REQUIRE(msg.find(L"Failed to retrieve message for \"MgInvalidArgumentException\"") != STRING::npos); + REQUIRE(msg.find(L"requires 0 argument(s) but 4 were supplied") != STRING::npos); + REQUIRE(msg.find(L"TestMisc::TestCase_ExceptionMessageFailureReason") != STRING::npos); + REQUIRE(msg.find(L"The resources cannot be the same.") != STRING::npos); + } +} + TEST_CASE("CommonExceptionMessages", "[Misc]") { try From svn_mapguide at osgeo.org Mon Nov 2 20:29:35 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 20:29:35 -0800 Subject: [mapguide-commits] r10253 - in branches/4.0/MgDev: Common/MapGuideCommon/System Portable/MgPortable/System Server/src/Core cmake/configs Message-ID: <20261103042935.D92141C840B@trac.osgeo.org> Author: jng Date: 2026-11-02 20:29:35 -0800 (Mon, 02 Nov 2026) New Revision: 10253 Modified: branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h branches/4.0/MgDev/Server/src/Core/serverconfig.ini branches/4.0/MgDev/cmake/configs/serverconfig.ini.in Log: Add more details on some serverconfig.ini log properties Fixes #1331 Modified: branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h =================================================================== --- branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h 2026-11-03 04:05:50 UTC (rev 10252) +++ branches/4.0/MgDev/Common/MapGuideCommon/System/ConfigProperties.h 2026-11-03 04:29:35 UTC (rev 10253) @@ -715,12 +715,12 @@ static const STRING AccessLogPropertyEnabled; /// value("Enabled") static const bool DefaultAccessLogPropertyEnabled; /// value(true) - /// The Access log's file name. NOTE: A log's filename may include the following special - /// characters: %d,%m,%y, standing for date, month, year. If one or all of these are used, - /// the special char will be replaced by the actual calendar value when the log file is - /// created. A new log file will be created when the shortest time period has elapsed. - /// That is, if the filename includes %d, a new log file will be created everyday. If the - /// filename contains both %m and %y, a new log file is created every month. + /// The Access log's file name. NOTE: A log's file name may include the following special + /// characters: %y, %m, %d, standing for year, month and day. A name that contains one of + /// them is archived when the period it names changes, so that %d archives and starts a new + /// log every day, %m every month, and %y every year. The period is taken from the first + /// special character in the name. The special characters are not replaced by the date in + /// the name of the log file in use; the date appears in the name of the archived file. static const STRING AccessLogPropertyFilename; /// value("Filename") static const STRING DefaultAccessLogPropertyFilename; /// value("Access.log") Modified: branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h 2026-11-03 04:05:50 UTC (rev 10252) +++ branches/4.0/MgDev/Portable/MgPortable/System/ConfigProperties.h 2026-11-03 04:29:35 UTC (rev 10253) @@ -513,12 +513,12 @@ static const STRING AccessLogPropertyEnabled; /// value("Enabled") static const bool DefaultAccessLogPropertyEnabled; /// value(true) - /// The Access log's file name. NOTE: A log's filename may include the following special - /// characters: %d,%m,%y, standing for date, month, year. If one or all of these are used, - /// the special char will be replaced by the actual calendar value when the log file is - /// created. A new log file will be created when the shortest time period has elapsed. - /// That is, if the filename includes %d, a new log file will be created everyday. If the - /// filename contains both %m and %y, a new log file is created every month. + /// The Access log's file name. NOTE: A log's file name may include the following special + /// characters: %y, %m, %d, standing for year, month and day. A name that contains one of + /// them is archived when the period it names changes, so that %d archives and starts a new + /// log every day, %m every month, and %y every year. The period is taken from the first + /// special character in the name. The special characters are not replaced by the date in + /// the name of the log file in use; the date appears in the name of the archived file. static const STRING AccessLogPropertyFilename; /// value("Filename") static const STRING DefaultAccessLogPropertyFilename; /// value("Access.log") Modified: branches/4.0/MgDev/Server/src/Core/serverconfig.ini =================================================================== --- branches/4.0/MgDev/Server/src/Core/serverconfig.ini 2026-11-03 04:05:50 UTC (rev 10252) +++ branches/4.0/MgDev/Server/src/Core/serverconfig.ini 2026-11-03 04:29:35 UTC (rev 10253) @@ -70,7 +70,20 @@ # MaxLogFileSize Max size allowed for the log files in # kilobytes (KB) # 0 < Value <= 2000000 +# Applies to all log files, and is used only +# when MaxLogFileSizeEnabled = 1 +# Example: 1024 = 1 MB # MaxLogFileSizeEnabled 0 = max size disabled, 1 = max size enabled +# When enabled, a log file that reaches +# MaxLogFileSize is archived: it is closed, +# renamed as described under LOG FILE NAMES +# below, and logging continues in a new file +# of the same name. Entries are never removed +# from a log and logging never stops; the +# server itself is not affected. Because the +# size is checked after each entry is +# written, an archived file can exceed the +# limit by one entry. # MentorDictionaryPath The path where the CS-Map Coordinate System Dictionaries are installed # MentorUserDictionaryPath The path where user defined CS-Map Coordinate System Dictionaries are stored # PreCacheMaps The list of maps to precache at server startup. @@ -480,6 +493,40 @@ DefaultTileSizeY = 300 ImageFormat = PNG +# ***************************************************************************** +# L O G F I L E N A M E S +# +# The Filename property of each log section names its log file. It is a file +# name only - a path is not allowed - and the file is created in the folder +# given by LogsPath in the GeneralProperties section. +# +# The name may contain these parameters: +# %y last two digits of the year +# %m month, 01 to 12 +# %d day of the month, 01 to 31 +# +# A parameter rolls the log over when the period it names changes: the log +# file in use is archived and logging continues in a new file. The period +# comes from the first parameter in the name, so a log that has to roll over +# daily puts %d first, as in "Access-%d%m%y.log"; "Access-%y%m%d.log" rolls +# over yearly, because %y is the first parameter. +# +# Archiving renames the file in use to +# _--_.log +# where is the part of the configured name before the extension, with +# the parameters removed. The parameters are not replaced by the date in the +# name of the file being written, so "Error-%d.log" is written to "Error-.log" +# and archived as "Error-_2026-11-3_.log". A name without +# parameters is not rolled over by the date. +# +# A log is also archived when it is enabled - at server startup, or through the +# Server Admin API - and the Parameters recorded in the header of the file in +# use no longer match the configured Parameters, so that each file's header +# describes its own entries. All archiving - by date, by size, and by +# parameter change - uses the name above, and archived files are never removed +# by the server. +# ***************************************************************************** + [AccessLogProperties] # ***************************************************************************** # A C C E S S L O G @@ -487,7 +534,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -501,7 +548,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -515,7 +562,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -529,7 +576,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -543,7 +590,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # Interval Time interval in seconds for when the server # writes the performance information to the log @@ -560,7 +607,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 0 @@ -574,7 +621,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 0 Modified: branches/4.0/MgDev/cmake/configs/serverconfig.ini.in =================================================================== --- branches/4.0/MgDev/cmake/configs/serverconfig.ini.in 2026-11-03 04:05:50 UTC (rev 10252) +++ branches/4.0/MgDev/cmake/configs/serverconfig.ini.in 2026-11-03 04:29:35 UTC (rev 10253) @@ -70,7 +70,20 @@ # MaxLogFileSize Max size allowed for the log files in # kilobytes (KB) # 0 < Value <= 2000000 +# Applies to all log files, and is used only +# when MaxLogFileSizeEnabled = 1 +# Example: 1024 = 1 MB # MaxLogFileSizeEnabled 0 = max size disabled, 1 = max size enabled +# When enabled, a log file that reaches +# MaxLogFileSize is archived: it is closed, +# renamed as described under LOG FILE NAMES +# below, and logging continues in a new file +# of the same name. Entries are never removed +# from a log and logging never stops; the +# server itself is not affected. Because the +# size is checked after each entry is +# written, an archived file can exceed the +# limit by one entry. # MentorDictionaryPath The path where the CS-Map Coordinate System Dictionaries are installed # MentorUserDictionaryPath The path where user defined CS-Map Coordinate System Dictionaries are stored # PreCacheMaps The list of maps to precache at server startup. @@ -482,6 +495,40 @@ DefaultTileSizeY = 300 ImageFormat = PNG +# ***************************************************************************** +# L O G F I L E N A M E S +# +# The Filename property of each log section names its log file. It is a file +# name only - a path is not allowed - and the file is created in the folder +# given by LogsPath in the GeneralProperties section. +# +# The name may contain these parameters: +# %y last two digits of the year +# %m month, 01 to 12 +# %d day of the month, 01 to 31 +# +# A parameter rolls the log over when the period it names changes: the log +# file in use is archived and logging continues in a new file. The period +# comes from the first parameter in the name, so a log that has to roll over +# daily puts %d first, as in "Access-%d%m%y.log"; "Access-%y%m%d.log" rolls +# over yearly, because %y is the first parameter. +# +# Archiving renames the file in use to +# _--_.log +# where is the part of the configured name before the extension, with +# the parameters removed. The parameters are not replaced by the date in the +# name of the file being written, so "Error-%d.log" is written to "Error-.log" +# and archived as "Error-_2026-11-3_.log". A name without +# parameters is not rolled over by the date. +# +# A log is also archived when it is enabled - at server startup, or through the +# Server Admin API - and the Parameters recorded in the header of the file in +# use no longer match the configured Parameters, so that each file's header +# describes its own entries. All archiving - by date, by size, and by +# parameter change - uses the name above, and archived files are never removed +# by the server. +# ***************************************************************************** + [AccessLogProperties] # ***************************************************************************** # A C C E S S L O G @@ -489,7 +536,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -503,7 +550,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -517,7 +564,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -531,7 +578,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 1 @@ -545,7 +592,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # Interval Time interval in seconds for when the server # writes the performance information to the log @@ -562,7 +609,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 0 @@ -576,7 +623,7 @@ # Property Name Description # ----------------------------------------------------------------------------- # Enabled 0 = log disabled, 1 = log enabled -# Filename Name of the log file +# Filename Name of the log file (see LOG FILE NAMES) # Parameters Log parameters # ***************************************************************************** Enabled = 0 From svn_mapguide at osgeo.org Mon Nov 2 20:56:17 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 20:56:17 -0800 Subject: [mapguide-commits] r10254 - in branches/4.0/MgDev/Server/src: Gws/GwsCommon/inc UnitTesting Message-ID: <20261103045617.208AD1C92A3@trac.osgeo.org> Author: jng Date: 2026-11-02 20:56:16 -0800 (Mon, 02 Nov 2026) New Revision: 10254 Modified: branches/4.0/MgDev/Server/src/Gws/GwsCommon/inc/GwsThreads.h branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj Log: Fix r10251 to build on Windows Modified: branches/4.0/MgDev/Server/src/Gws/GwsCommon/inc/GwsThreads.h =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsCommon/inc/GwsThreads.h 2026-11-03 04:29:35 UTC (rev 10253) +++ branches/4.0/MgDev/Server/src/Gws/GwsCommon/inc/GwsThreads.h 2026-11-03 04:56:16 UTC (rev 10254) @@ -22,7 +22,10 @@ #define INVALID_HANDLE 0 #ifdef _WIN32 -#include +// windows.h, not winbase.h: winbase.h is not a standalone header. It expects +// windef.h and winnt.h to have been included first, and this header uses types +// from both (CRITICAL_SECTION, and the HANDLE-based semaphore below). +#include #else #include #endif Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-03 04:29:35 UTC (rev 10253) +++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-11-03 04:56:16 UTC (rev 10254) @@ -104,7 +104,7 @@ true - ACEd.lib;FDO.lib;%(AdditionalDependencies) + ACEd.lib;FDO.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)MgUnitTestingd.dll ..\..\..\Oem\ACE\ACE_wrappers\lib\$(Configuration);..\..\..\Oem\FDO\lib;%(AdditionalLibraryDirectories) true @@ -130,7 +130,7 @@ true - ACEd.lib;FDO.lib;%(AdditionalDependencies) + ACEd.lib;FDO.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)MgUnitTestingd.dll ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true @@ -155,7 +155,7 @@ true - ACE.lib;FDO.lib;%(AdditionalDependencies) + ACE.lib;FDO.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)MgUnitTesting.dll ..\..\..\Oem\ACE\ACE_wrappers\lib\$(Configuration);..\..\..\Oem\FDO\lib;%(AdditionalLibraryDirectories) true @@ -182,7 +182,7 @@ true - ACE.lib;FDO.lib;%(AdditionalDependencies) + ACE.lib;FDO.lib;FDOCommon.lib;%(AdditionalDependencies) $(OutDir)MgUnitTesting.dll ..\..\..\Oem\ACE\ACE_wrappers\lib64\$(Configuration);..\..\..\Oem\FDO\lib64\$(Configuration)64;%(AdditionalLibraryDirectories) true From svn_mapguide at osgeo.org Mon Nov 2 21:06:46 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 21:06:46 -0800 Subject: [mapguide-commits] r10255 - in branches/4.0/MgDev/Server/src: Common/Base Common/Manager Services/Resource UnitTesting Message-ID: <20261103050647.196321C9869@trac.osgeo.org> Author: jng Date: 2026-11-02 21:06:46 -0800 (Mon, 02 Nov 2026) New Revision: 10255 Modified: branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp Log: Improve quality of some BDB errors masked as out of memory errors. Fixes #2292 Modified: branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp =================================================================== --- branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp 2026-11-03 04:56:16 UTC (rev 10254) +++ branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp 2026-11-03 05:06:46 UTC (rev 10255) @@ -229,6 +229,7 @@ } STRING message = except->GetExceptionMessage(locale); + message += MgLogManager::FormatExceptionInfo(except); STRING details = except->GetDetails(locale); STRING stackTrace = except->GetStackTrace(locale); Modified: branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp =================================================================== --- branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp 2026-11-03 04:56:16 UTC (rev 10254) +++ branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp 2026-11-03 05:06:46 UTC (rev 10255) @@ -20,6 +20,16 @@ #include "LogManager.h" #include "LogThread.h" +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + const int MAX_BUF = 16384; const int MAX_LINES = 512; const int SECONDSINDAY = 86400; @@ -2188,6 +2198,115 @@ QueueLogEntry(mltTrace, logEntry, LM_INFO); } +/////////////////////////////////////////////////////////////////////////////// +/// Reads the process's virtual size and resident set size, in bytes. +/// A negative value means "not available". +/// +static void GetProcessMemoryUsage(INT64& virtualMemory, INT64& workingSet) +{ + virtualMemory = -1; + workingSet = -1; + +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS counters; + counters.cb = sizeof(counters); + if (::GetProcessMemoryInfo(::GetCurrentProcess(), &counters, sizeof(counters))) + { + workingSet = (INT64)counters.WorkingSetSize; + virtualMemory = (INT64)counters.PagefileUsage; + } +#else + FILE* fp = ::fopen("/proc/self/status", "rb"); + if (NULL != fp) + { + char buffer[4096]; + size_t count = ::fread(buffer, 1, sizeof(buffer) - 1, fp); + ::fclose(fp); + buffer[count] = '\0'; + + const char* value = ::strstr(buffer, "VmSize:"); + if (NULL != value) + { + virtualMemory = strtoll(value + 7, NULL, 10) * 1024; + } + + value = ::strstr(buffer, "VmRSS:"); + if (NULL != value) + { + workingSet = strtoll(value + 6, NULL, 10) * 1024; + } + } +#endif +} + +/////////////////////////////////////////////////////////////////////////////// +/// \brief +/// Returns the facts about an exception that the log entry would otherwise +/// lose: its code and, when set, its native error code. +/// +/// For allocation failures the process's memory state is added as well, since +/// Berkeley DB reports a full lock table as ENOMEM too - the values show +/// whether the process was anywhere near a memory limit when it happened. +/// +STRING MgLogManager::FormatExceptionInfo(MgException* except) +{ + STRING info; + + try + { + if (NULL == except) + { + return info; + } + + STRING code = except->GetExceptionCode(); + if (!code.empty()) + { + info += L" [code="; + info += code; + info += L"]"; + } + + INT64 nativeErrorCode = except->GetNativeErrorCode(); + if (0 != nativeErrorCode) + { + STRING value; + MgUtil::Int64ToString(nativeErrorCode, value); + + info += L" [nativeError="; + info += value; + info += L"]"; + } + + bool allocationFailure = (MgExceptionCodes::MgOutOfMemoryException == code) + || (0 != nativeErrorCode && ENOMEM == nativeErrorCode); + + if (allocationFailure) + { + INT64 virtualMemory = -1; + INT64 workingSet = -1; + GetProcessMemoryUsage(virtualMemory, workingSet); + + STRING vmValue; + STRING rssValue; + MgUtil::Int64ToString(virtualMemory, vmValue); + MgUtil::Int64ToString(workingSet, rssValue); + + info += L" [vm="; + info += vmValue; + info += L" rss="; + info += rssValue; + info += L"]"; + } + } + catch (...) + { + // Formatting diagnostics must never lose the entry being logged. + } + + return info; +} + void MgLogManager::LogSystemErrorEntry(MgException* except) { if (NULL != except) @@ -2197,6 +2316,7 @@ STRING locale = serverManager->GetDefaultMessageLocale(); STRING message = except->GetExceptionMessage(locale); + message += MgLogManager::FormatExceptionInfo(except); STRING details = except->GetDetails(locale); STRING stackTrace = except->GetStackTrace(locale); Modified: branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h =================================================================== --- branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h 2026-11-03 04:56:16 UTC (rev 10254) +++ branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h 2026-11-03 05:06:46 UTC (rev 10255) @@ -309,6 +309,7 @@ void Initialize(); void LoadConfigurationProperties(); STRING GetLogsPath(); + static STRING FormatExceptionInfo(MgException* except); void SetMaximumLogSize(INT32 size); void SetLogDelimiter(CREFSTRING delimiter); STRING GetLogDelimiter(); Modified: branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp =================================================================== --- branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp 2026-11-03 04:56:16 UTC (rev 10254) +++ branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp 2026-11-03 05:06:46 UTC (rev 10255) @@ -18,6 +18,125 @@ #include "ResourceServiceDefs.h" #include "DbEnvironment.h" +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +/////////////////////////////////////////////////////////////////////////////// +/// Berkeley DB reports its own diagnostics through an error callback rather +/// than through the exception it returns: "Lock table is out of available +/// locker entries", "unable to allocate memory for the lock table", "PANIC: +/// fatal region error detected" and so on. Without a callback those messages +/// go to stderr and are lost, and the exception itself only carries +/// strerror() of the error number - which is how an exhausted lock table ends +/// up logged as "Cannot allocate memory". +/// +/// The callback can run while Berkeley DB holds region mutexes and while the +/// process cannot allocate memory, so it writes to a pre-opened file +/// descriptor: no allocation, no logging framework, nothing that can throw. +/// The file is truncated once per process, so it describes the current run. +/// +static const wchar_t* MG_DB_ERROR_LOG_FILE = L"DbError.log"; + +#ifdef _WIN32 +#define MG_DB_ERROR_OPEN _open +#define MG_DB_ERROR_WRITE _write +#define MG_DB_ERROR_OPEN_FLAGS (_O_CREAT|_O_WRONLY|_O_TRUNC|_O_APPEND|_O_BINARY) +#else +#define MG_DB_ERROR_OPEN ::open +#define MG_DB_ERROR_WRITE ::write +#define MG_DB_ERROR_OPEN_FLAGS (O_CREAT|O_WRONLY|O_TRUNC|O_APPEND) +#endif + +static int OpenDbErrorLog() +{ + try + { + STRING logsPath; + MgConfiguration* configuration = MgConfiguration::GetInstance(); + if (NULL != configuration) + { + configuration->GetStringValue( + MgConfigProperties::GeneralPropertiesSection, + MgConfigProperties::GeneralPropertyLogsPath, + logsPath, + MgConfigProperties::DefaultGeneralPropertyLogsPath); + } + + if (logsPath.empty()) + { + return -1; + } + + // LogsPath is a directory in the configuration; it need not end with a + // separator (LogManager normalises it the same way). + MgFileUtil::AppendSlashToEndOfPath(logsPath); + MgFileUtil::CreateDirectory(logsPath, false, true); + logsPath += MG_DB_ERROR_LOG_FILE; + + string path = MgUtil::WideCharToMultiByte(logsPath); + return MG_DB_ERROR_OPEN(path.c_str(), MG_DB_ERROR_OPEN_FLAGS, 0644); + } + catch (...) + { + // Diagnostics must never prevent a repository from being opened. + return -1; + } +} + +static int GetDbErrorLog() +{ + // C++11 makes the initialization of a function-local static thread-safe, so + // the file is opened exactly once per process regardless of how many + // environments (one per session with FilePerSession) are created. + static const int fd = OpenDbErrorLog(); + return fd; +} + +static const char* GetDbErrorPrefix(CREFSTRING repositoryType) +{ + if (MgRepositoryType::Session == repositoryType) + { + return "Session"; + } + if (MgRepositoryType::Site == repositoryType) + { + return "Site"; + } + return "Library"; +} + +static void WriteDbErrorMessage(const DbEnv* dbEnv, const char* errPrefix, const char* message) +{ + int fd = GetDbErrorLog(); + if (fd < 0 || NULL == message) + { + return; + } + + char line[1024]; + int len = (NULL != errPrefix) + ? snprintf(line, sizeof(line), "[DB] %s: %s\n", errPrefix, message) + : snprintf(line, sizeof(line), "[DB] %s\n", message); + + if (len <= 0) + { + return; + } + + size_t count = ((size_t)len < sizeof(line)) ? (size_t)len : sizeof(line) - 1; + + // There is nowhere left to report a failed write to. + int written = (int)MG_DB_ERROR_WRITE(fd, line, (unsigned int)count); + ACE_UNUSED_ARG(written); +} + ///---------------------------------------------------------------------------- /// /// Constructs the object. @@ -194,6 +313,13 @@ environmentFlags |= DB_INIT_CDB; } + // Route Berkeley DB's own diagnostics to the logs directory. Set before the + // environment is opened so that failures during open (for example "unable + // to allocate memory for the lock table") are captured too. + GetDbErrorLog(); + m_dbEnv.set_errpfx(GetDbErrorPrefix(repositoryType)); + m_dbEnv.set_errcall(&WriteDbErrorMessage); + m_dbEnv.open(home.c_str(), environmentFlags, 0); m_opened = true; @@ -217,6 +343,11 @@ m_xmlMan.setDefaultContainerFlags(containerFlags); + // Re-assert the error callback: DbXml installs its own (std::cout based) + // handler on an adopted environment when it is built with DB_SET_ERRCALL. + m_dbEnv.set_errpfx(GetDbErrorPrefix(repositoryType)); + m_dbEnv.set_errcall(&WriteDbErrorMessage); + MG_RESOURCE_SERVICE_CATCH_AND_THROW(L"MgDbEnvironment.MgDbEnvironment") } Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp 2026-11-03 04:56:16 UTC (rev 10254) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp 2026-11-03 05:06:46 UTC (rev 10255) @@ -23,7 +23,9 @@ #include "TestServiceFactory.h" #include "catch.hpp" +#include + const wchar_t TestName[] = L"TestName.log"; const wchar_t TestName2[] = L"TestName2.log"; const wchar_t NewTestName[] = L"SuperCoolNewTestName.log"; @@ -1860,3 +1862,40 @@ REQUIRE(logContents == contents); } + +///---------------------------------------------------------------------------- +/// Test Case Description: +/// +/// The error log records the exception code and native error code alongside +/// the message, plus the process memory state for allocation failures. Without +/// it a DB error reported as ENOMEM ("Cannot allocate memory") cannot be told +/// apart from the process actually running out of memory. +///---------------------------------------------------------------------------- +TEST_CASE("FormatExceptionInfo", "[LogManager]") +{ + // A database error reported as ENOMEM carries the code, the native error + // and the process memory state. + Ptr dbException = new MgException( + MgExceptionCodes::MgDbXmlException, L"TestLogManager.FormatExceptionInfo", + __LINE__, __WFILE__, NULL, L"", NULL); + dbException->SetNativeErrorCode(ENOMEM); + + STRING info = MgLogManager::FormatExceptionInfo(dbException); + REQUIRE(info.find(L"[code=MgDbXmlException]") != STRING::npos); + REQUIRE(info.find(L"[nativeError=") != STRING::npos); + REQUIRE(info.find(L"[vm=") != STRING::npos); + + // An exception with no native error code reports neither the native error + // nor the memory state. + Ptr plainException = new MgException( + MgExceptionCodes::MgNullReferenceException, L"TestLogManager.FormatExceptionInfo", + __LINE__, __WFILE__, NULL, L"", NULL); + + STRING plainInfo = MgLogManager::FormatExceptionInfo(plainException); + REQUIRE(plainInfo.find(L"[code=MgNullReferenceException]") != STRING::npos); + REQUIRE(plainInfo.find(L"[nativeError=") == STRING::npos); + REQUIRE(plainInfo.find(L"[vm=") == STRING::npos); + + // A null exception yields nothing rather than throwing. + REQUIRE(MgLogManager::FormatExceptionInfo(NULL).empty()); +} From svn_mapguide at osgeo.org Mon Nov 2 21:32:44 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 21:32:44 -0800 Subject: [mapguide-commits] r10256 - in branches/4.0/MgDev: Common/Renderers Common/Stylization Server/src/UnitTesting Message-ID: <20261103053244.4295B1CA5CB@trac.osgeo.org> Author: jng Date: 2026-11-02 21:32:43 -0800 (Mon, 02 Nov 2026) New Revision: 10256 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp branches/4.0/MgDev/Common/Renderers/KmlRenderer.h branches/4.0/MgDev/Common/Stylization/Renderer.h branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp branches/4.0/MgDev/Common/Stylization/StylizationEngine.h branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp branches/4.0/MgDev/Server/src/UnitTesting/TestServiceFactory.cpp Log: Fix excessive KML placemarks being rendered under composite symbolization. Fixes #2488 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 05:32:43 UTC (rev 10256) @@ -39,7 +39,7 @@ m_mainContent(kmlContent), m_kmlContent(kmlContent), m_styleContent(NULL), - m_featureCount(0), + m_placemarkOpen(false), m_mapInfo(NULL), m_layerInfo(NULL), m_featureClassInfo(NULL), @@ -72,7 +72,7 @@ double /*metersPerUnit*/, CSysTransformer* /*xformToLL*/) { - m_featureCount = 0; + m_placemarkOpen = false; // remember the map info m_mapInfo = mapInfo; @@ -89,7 +89,7 @@ void KmlRenderer::StartLayer(RS_LayerUIInfo* layerInfo, RS_FeatureClassInfo* classInfo) { - m_featureCount = 0; + m_placemarkOpen = false; m_layerInfo = layerInfo; m_featureClassInfo = classInfo; ClearThemes(); @@ -100,11 +100,7 @@ void KmlRenderer::EndLayer() { //close the very last feature - if (m_featureCount > 0) - { - //end last feature - EndPlacemark(); - } + EndPlacemark(); m_kmlContent = m_mainContent; //write out the features for each theme @@ -128,6 +124,9 @@ void KmlRenderer::EndPlacemark() { + if (!m_placemarkOpen) + return; + //a placemark has one name at most, and an empty one shows as a blank entry //next to the real name in Google Earth, so only write it when there is one if (!m_placemarkName.empty()) @@ -138,11 +137,12 @@ m_placemarkName.clear(); } m_kmlContent->WriteString(""); + m_placemarkOpen = false; } void KmlRenderer::StartFeature(RS_FeatureReader* /*feature*/, - bool /*initialPass*/, + bool initialPass, const RS_String* tooltip, const RS_String* url, const RS_String* theme, @@ -150,56 +150,64 @@ double zExtrusion, RS_ElevationType zOffsetType) { - if (m_featureCount > 0) + KmlContent* content = m_mainContent; + if (theme != NULL && !theme->empty()) { - //end last feature - EndPlacemark(); - } - if (theme == NULL || theme->empty()) - { - m_kmlContent = m_mainContent; - } - else - { ThemeMap::iterator iter = m_themeMap.find(*theme); if (iter != m_themeMap.end()) { - m_kmlContent = (*iter).second; + content = (*iter).second; } else { - m_kmlContent = new KmlContent(); - m_themeMap[*theme] = m_kmlContent; + content = new KmlContent(); + m_themeMap[*theme] = content; } } - m_placemarkName.clear(); - m_kmlContent->WriteString(""); - if ((tooltip != NULL && tooltip->length() > 0) || - (url != NULL && url->length() > 0)) + + //a feature is stylized once per composite type style, so this is called once + //per style of the same feature and only the first of those passes is the + //initial one. The placemark belongs to the feature, not to the style pass, so + //the remaining passes add to the placemark the initial pass opened. A pass + //that moves the feature to another theme cannot continue it, because a + //placemark cannot span the folders the themes are written into. + if (m_placemarkOpen && (initialPass || content != m_kmlContent)) { - m_kmlContent->WriteString(""); - m_kmlContent->WriteString("length() > 0) + EndPlacemark(); + } + m_kmlContent = content; + + if (!m_placemarkOpen) + { + m_placemarkName.clear(); + m_kmlContent->WriteString(""); + m_placemarkOpen = true; + if ((tooltip != NULL && tooltip->length() > 0) || + (url != NULL && url->length() > 0)) { - //replace line breaks - RS_String newTT(*tooltip); - RS_String::size_type lb = newTT.find(L"\\n"); - while (lb != RS_String::npos) + m_kmlContent->WriteString(""); + m_kmlContent->WriteString("length() > 0) { - newTT = newTT.replace(lb, 2, L"
"); - lb = newTT.find(L"\\n", lb); + //replace line breaks + RS_String newTT(*tooltip); + RS_String::size_type lb = newTT.find(L"\\n"); + while (lb != RS_String::npos) + { + newTT = newTT.replace(lb, 2, L"
"); + lb = newTT.find(L"\\n", lb); + } + m_kmlContent->WriteString(newTT, false); + m_kmlContent->WriteString("
", false); } - m_kmlContent->WriteString(newTT, false); - m_kmlContent->WriteString("
", false); + if (url != NULL && url->length() > 0) + { + m_kmlContent->WriteString(*url, false); + } + m_kmlContent->WriteString("]]>"); + m_kmlContent->WriteString("
"); } - if (url != NULL && url->length() > 0) - { - m_kmlContent->WriteString(*url, false); - } - m_kmlContent->WriteString("]]>"); - m_kmlContent->WriteString("
"); } - m_featureCount++; // Elevation / Extrusion settings m_elevation = zExtrusion + zOffset; @@ -507,6 +515,16 @@ } +bool KmlRenderer::RequiresCompositeLineStyleSeparation() +{ + //separating a rule's line styles into one styling pass each is what the + //screen renderers need, but it would draw a feature more than once and so + //give it a placemark per pass - a placemark holds all of a rule's line + //styles, so they can be stylized together (ticket #2488) + return false; +} + + bool KmlRenderer::SupportsZ() { // although KmlRenderer supports elevation, it ignores Z values Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 05:32:43 UTC (rev 10256) @@ -120,6 +120,8 @@ virtual bool RequiresLabelClipping(); + virtual bool RequiresCompositeLineStyleSeparation(); + virtual bool SupportsZ(); //////////////////////////////////////////////// @@ -183,7 +185,9 @@ KmlContent* m_kmlContent; KmlContent* m_mainContent; KmlContent* m_styleContent; - int m_featureCount; + // true while a has been written and not yet closed. A feature's + // style passes share the placemark its initial pass opened. + bool m_placemarkOpen; RS_MapUIInfo* m_mapInfo; RS_LayerUIInfo* m_layerInfo; RS_FeatureClassInfo* m_featureClassInfo; Modified: branches/4.0/MgDev/Common/Stylization/Renderer.h =================================================================== --- branches/4.0/MgDev/Common/Stylization/Renderer.h 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Common/Stylization/Renderer.h 2026-11-03 05:32:43 UTC (rev 10256) @@ -76,7 +76,11 @@ /// /// Indicates the renderer should process all subsequent geometry - /// as part of a single logical feature. + /// as part of a single logical feature. When a feature is stylized by + /// several composite type styles, this is called once per style and + /// initialPass is true only for the first of those calls, which is + /// where a renderer that groups a feature's output (the KML + /// placemark, for example) opens its group. /// virtual void StartFeature(RS_FeatureReader* feature, bool initialPass, Modified: branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp =================================================================== --- branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp 2026-11-03 05:32:43 UTC (rev 10256) @@ -166,12 +166,20 @@ } // stylize once for each composite type style + bool featureStarted = false; for (size_t i=0; i* symbolInstances = &rule->symbolInstances; if (symbolInstances->size() == 0) - return; + return true; double mm2sud = m_serenderer->GetScreenUnitsPerMillimeterDevice(); double mm2suw = m_serenderer->GetScreenUnitsPerMillimeterWorld(); @@ -1103,7 +1112,7 @@ // if the clipped buffer is NULL (completely clipped) just move on to // the next feature if (!lbc) - return; + return true; // otherwise continue processing with the clipped buffer lb = lbc; @@ -1250,6 +1259,8 @@ // free clipped line buffer if the geometry was clipped if (spClipLB.get()) LineBufferPool::FreeLineBuffer(m_pool, spClipLB.release()); + + return true; } Modified: branches/4.0/MgDev/Common/Stylization/StylizationEngine.h =================================================================== --- branches/4.0/MgDev/Common/Stylization/StylizationEngine.h 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Common/Stylization/StylizationEngine.h 2026-11-03 05:32:43 UTC (rev 10256) @@ -78,7 +78,9 @@ int saveHeight); // Stylizes the current feature on the reader using the supplied composite type style. - void Stylize(RS_FeatureReader* reader, + // Returns true if the renderer was given the feature - the style's rules can match + // nothing, in which case no StartFeature() is issued for it. + bool Stylize(RS_FeatureReader* reader, FdoExpressionEngine* exec, LineBuffer* geometry, CompositeTypeStyle* style, Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 05:32:43 UTC (rev 10256) @@ -484,3 +484,220 @@ REQUIRE(CountOccurrences(placemark, "") == 1); REQUIRE(placemark.find("") != std::string::npos); } + +////////////////////////////////////////////////////////////////////////////// +// Kml renderer placemark grouping across composite style passes (ticket #2488) +// +// StylizationEngine::StylizeVectorLayer() stylizes a feature once per composite +// type style, so StartFeature() is called several times for the same feature and +// only the first of those calls is the initial pass. A placemark belongs to the +// feature, not to the style pass, so all of a feature's style passes have to land +// in one - otherwise GetFeaturesKml() reports one placemark per style. + +namespace +{ + // Everything in the KML that is not inside a element + std::string GetContentOutsidePlacemarks(const std::string& kml) + { + static const std::string startTag(""); + static const std::string endTag(""); + + std::string content; + std::string::size_type pos = 0; + while (true) + { + std::string::size_type start = kml.find(startTag, pos); + std::string::size_type end = (start == std::string::npos)? std::string::npos : kml.find(endTag, start); + if (end == std::string::npos) + { + content.append(kml, pos, std::string::npos); + break; + } + content.append(kml, pos, start - pos); + pos = end + endTag.length(); + } + return content; + } + + // Renders one point feature, calling StartFeature() once per composite type + // style pass, in the order StylizationEngine does it. A feature whose leading + // styles match no rule has no call for those styles, so initialPass marks the + // first pass it actually reaches the renderer for. + // + // ProcessMarker() rather than ProcessPolyline() because it is the one the + // renderer exports: the geometry adapters reach the other Process* methods + // through the vtable, so only ProcessMarker() is linkable from this module + // when KmlRenderer lives in its own DLL (the Windows build). + void StylizePointFeature(KmlRenderer& renderer, + int stylePasses, + bool initialPassOnFirst, + double y, + const RS_String* theme = NULL) + { + LineBuffer lb(2); + lb.MoveTo(-87.70, y); + + RS_MarkerDef mdef; + + for (int pass = 0; pass < stylePasses; ++pass) + { + renderer.StartFeature(NULL, initialPassOnFirst && pass == 0, NULL, NULL, theme, 0.0, 0.0, RS_ElevationType_RelativeToGround); + renderer.ProcessMarker(&lb, mdef, true, NULL); + } + } + + void StartRailLayer(KmlRenderer& renderer) + { + RS_UIGraphic graphic(NULL, 0, L""); + RS_LayerUIInfo layerInfo(L"Rail", L"Library://UnitTests/Layers/MultiCTS.LayerDefinition", + L"", true, true, false, L"", L"", true, true, 0.0, graphic); + RS_FeatureClassInfo classInfo(L"Rail", L"Library://UnitTests/Data/Rail.FeatureSource", L"SHPGEOM"); + renderer.StartLayer(&layerInfo, &classInfo); + } +} + +TEST_CASE("KmlCompositeStyleWritesOnePlacemark", "[KmlService]") +{ + KmlContent kmlContent; + RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); + StartRailLayer(renderer); + + // one feature, stylized by three composite type styles + StylizePointFeature(renderer, 3, true, 43.75); + + renderer.EndLayer(); + + std::string kml = kmlContent.GetString(); + INFO(kml); + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 1); + REQUIRE(CountOccurrences(placemarks.front(), "") == 3); + REQUIRE(GetContentOutsidePlacemarks(kml).find("") == std::string::npos); +} + +TEST_CASE("KmlCompositeStyleKeepsFeaturesApart", "[KmlService]") +{ + KmlContent kmlContent; + RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); + StartRailLayer(renderer); + + // two features, two composite type styles each + StylizePointFeature(renderer, 2, true, 43.75); + StylizePointFeature(renderer, 2, true, 43.78); + + renderer.EndLayer(); + + std::string kml = kmlContent.GetString(); + INFO(kml); + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 2); + for (size_t i = 0; i < placemarks.size(); ++i) + { + // each feature's style passes stay in its own placemark + REQUIRE(CountOccurrences(placemarks[i], "") == 2); + } + REQUIRE(GetContentOutsidePlacemarks(kml).find("") == std::string::npos); +} + +TEST_CASE("KmlCompositeStylePlacemarkDoesNotSpanThemes", "[KmlService]") +{ + KmlContent kmlContent; + RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); + StartRailLayer(renderer); + + // a feature whose two composite type styles carry different legend labels: + // each theme is written into its own folder, and a placemark cannot be left + // open across folders, so the second theme starts a placemark of its own + RS_String themeA(L"Single Track"); + RS_String themeB(L"Dual Track"); + StylizePointFeature(renderer, 1, true, 43.75, &themeA); + StylizePointFeature(renderer, 1, false, 43.75, &themeB); + + renderer.EndLayer(); + + std::string kml = kmlContent.GetString(); + INFO(kml); + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 2); + for (size_t i = 0; i < placemarks.size(); ++i) + { + REQUIRE(CountOccurrences(placemarks[i], "") == 1); + } + REQUIRE(GetContentOutsidePlacemarks(kml).find("") == std::string::npos); +} + +TEST_CASE("GetFeaturesKmlCompositeStylePerFeature", "[KmlService]") +{ + try + { + Ptr m_svcKml = TestServiceFactory::CreateKmlService(); + Ptr m_svcResource = TestServiceFactory::CreateResourceService(); + + Ptr extents = new MgEnvelope(-87.764986990962839, 43.691398128787782, + -87.695521510899724, 43.797520000480347); + STRING agentUri = L"http://myserver/mapguide/mapagent/mapagent.fcgi"; + STRING format = L"KML"; + + try + { + // MultiCTS.LayerDefinition carries the Rail features, the same ones + // Rail.LayerDefinition draws, but with three composite type styles - so + // each feature is stylized three times. The KML must still carry one + // placemark per feature, and therefore as many placemarks as the layer + // with a single style produces over the same features (ticket #2488). + Ptr compositeLdf = new MgResourceIdentifier(L"Library://UnitTests/Layers/MultiCTS.LayerDefinition"); + Ptr compositeLayer = new MgLayer(compositeLdf, m_svcResource); + + Ptr lineLdf = new MgResourceIdentifier(L"Library://UnitTests/Layers/Rail.LayerDefinition"); + Ptr lineLayer = new MgLayer(lineLdf, m_svcResource); + + Ptr compositeReader = m_svcKml->GetFeaturesKml(compositeLayer, extents, 796, 717, 96.0, 0, agentUri, format); + Ptr lineReader = m_svcKml->GetFeaturesKml(lineLayer, extents, 796, 717, 96.0, 0, agentUri, format); + + // the KML mime type is not one MgByteSink::ToStringUtf8() will read + compositeReader->GetByteSource()->SetMimeType(L"text/xml"); + lineReader->GetByteSource()->SetMimeType(L"text/xml"); + std::string compositeKml = MgUtil::WideCharToMultiByte(compositeReader->ToString()); + std::string lineKml = MgUtil::WideCharToMultiByte(lineReader->ToString()); + + int compositePlacemarks = CountOccurrences(compositeKml, ""); + int linePlacemarks = CountOccurrences(lineKml, ""); + + INFO(compositeKml); + INFO(lineKml); + REQUIRE(linePlacemarks > 0); + REQUIRE(compositePlacemarks == linePlacemarks); + } + catch (MgException* e) + { + if (e->GetExceptionCode() == MgExceptionCodes::MgConnectionFailedException) + { + SAFE_RELEASE(e); + ACE_DEBUG((LM_INFO, ACE_TEXT("\nTestCase_GetFeaturesKmlCompositeStylePerFeature skipped because unable to connect to test server.\n"))); + + // We need to update the mapguide server status so that it can be used again because we have used the + // same exception class for servers outside the one actually executing this code. + MgSiteManager* siteManager = MgSiteManager::GetInstance(); + Ptr siteInfo = siteManager->GetSiteInfo(0); + siteInfo->SetStatus(MgSiteInfo::Ok); + } + else + { + e->Raise(); + } + } + } + catch(MgException* e) + { + STRING message = e->GetDetails(TestServiceFactory::TEST_LOCALE); + SAFE_RELEASE(e); + FAIL(MG_WCHAR_TO_CHAR(message.c_str())); + } + catch(...) + { + throw; + } +} Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestServiceFactory.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestServiceFactory.cpp 2026-11-03 05:06:46 UTC (rev 10255) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestServiceFactory.cpp 2026-11-03 05:32:43 UTC (rev 10256) @@ -465,6 +465,13 @@ Ptr ldfrdr3 = ldfsrc3->GetReader(); svcResource->SetResource(ldfres3, ldfrdr3, nullptr); + //the features of Rail.LayerDefinition, but with several composite type + //styles, so each feature is stylized once per style (ticket #2488) + Ptr ldfres4 = new MgResourceIdentifier(L"Library://UnitTests/Layers/MultiCTS.LayerDefinition"); + Ptr ldfsrc4 = new MgByteSource(L"../UnitTestFiles/UT_MultiCTS.ldf", false); + Ptr ldfrdr4 = ldfsrc4->GetReader(); + svcResource->SetResource(ldfres4, ldfrdr4, nullptr); + //publish the feature sources Ptr fsres1 = new MgResourceIdentifier(L"Library://UnitTests/Data/HydrographicPolygons.FeatureSource"); Ptr fssrc1 = new MgByteSource(L"../UnitTestFiles/UT_HydrographicPolygons.fs", false); @@ -1735,6 +1742,9 @@ Ptr ldfres3 = new MgResourceIdentifier(L"Library://UnitTests/Layers/Rail.LayerDefinition"); svcResource->DeleteResource(ldfres3); + Ptr ldfres4 = new MgResourceIdentifier(L"Library://UnitTests/Layers/MultiCTS.LayerDefinition"); + svcResource->DeleteResource(ldfres4); + //delete the feature sources Ptr fsres1 = new MgResourceIdentifier(L"Library://UnitTests/Data/HydrographicPolygons.FeatureSource"); svcResource->DeleteResource(fsres1); From svn_mapguide at osgeo.org Mon Nov 2 21:51:50 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Mon, 2 Nov 2026 21:51:50 -0800 Subject: [mapguide-commits] r10257 - in branches/4.0/MgDev: Common/Renderers Common/Stylization Server/src/UnitTesting Message-ID: <20261103055150.DC55E1CB196@trac.osgeo.org> Author: jng Date: 2026-11-02 21:51:50 -0800 (Mon, 02 Nov 2026) New Revision: 10257 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp branches/4.0/MgDev/Common/Renderers/KmlRenderer.h branches/4.0/MgDev/Common/Stylization/Renderer.h branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp branches/4.0/MgDev/Common/Stylization/StylizationEngine.h branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp Log: #2488: Signal the feature boundary with EndFeature instead of redefining initialPass r10256 made a feature's style passes share the placemark opened by its initial pass by redefining the initialPass that StartFeature receives as "the first style pass that reaches the renderer", in each rendering pass. initialPass is not private to the KML renderer: FeatureInfoRenderer and FeaturePropRenderer (QueryFeatureGeometry and QueryFeatureProperties) ignore every pass that does not set it, and FeaturePropRenderer appends one record for each pass it accepts, so a layer needing a symbol or instance rendering pass would have reported every feature once per pass in those results. It would also have changed, in an unrelated service, whether a feature whose leading composite styles match no rule appears in them at all. The boundary is now signalled explicitly, and initialPass keeps the values every other renderer already saw: * Common/Stylization/Renderer.h: declare EndFeature(), the hook the interface had reserved in a TODO, with an empty body so no renderer is obliged to implement it. * Common/Stylization/StylizationEngine.cpp/.h: StylizeVectorLayer() calls EndFeature() once per feature, after its last style pass. Stylize() is void again and initialPass is back to its original definition (first style of the first rendering pass). * Common/Renderers/KmlRenderer.cpp/.h: EndFeature() closes the placemark, and StartFeature() opens one on the initial pass or when none is open - which is what a feature re-stylized by a later rendering pass sees, so those passes no longer merge features into one placemark. A pass that moves the feature to another theme still starts one, because a placemark cannot span the folders the themes are written into. * Server/src/UnitTesting/TestKmlService.cpp: the composite-style cases drive EndFeature() the way the engine does, a new KmlCompositeStyleSeparatesFeaturesInLaterRenderingPass covers a pass in which no style pass is initial, and the layer and feature class info the renderer points at are held by a RailLayer fixture that outlives it. Reverting the renderer to its pre-#2488 form fails three of the cases (3 vs 1 and 4 vs 2 placemarks, and 7485 vs 2495 for GetFeaturesKml over MultiCTS against the single-style Rail layer). A composite layer whose first style matches no feature was checked by temporarily filtering that style (reverted): one placemark per feature either way. Verified on ubuntu22 x64 in the Debug/ASan tree: KmlService passes with "All tests passed (32 assertions in 13 test cases)" and no LeakSanitizer record, and MappingService, RenderingService, Stylization and RasterStylization - the suites that drive the stylization engine - all pass. Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.cpp 2026-11-03 05:51:50 UTC (rev 10257) @@ -122,6 +122,12 @@ } +void KmlRenderer::EndFeature() +{ + EndPlacemark(); +} + + void KmlRenderer::EndPlacemark() { if (!m_placemarkOpen) @@ -166,11 +172,12 @@ } //a feature is stylized once per composite type style, so this is called once - //per style of the same feature and only the first of those passes is the - //initial one. The placemark belongs to the feature, not to the style pass, so - //the remaining passes add to the placemark the initial pass opened. A pass - //that moves the feature to another theme cannot continue it, because a - //placemark cannot span the folders the themes are written into. + //per style of the same feature and EndFeature() closes the feature when its + //last pass is done. The placemark belongs to the feature, not to the style + //pass, so only the initial pass - or a pass with no placemark open, which is + //what a later rendering pass sees - starts one. A pass that moves the feature + //to another theme starts one too, because a placemark cannot span the folders + //the themes are written into. if (m_placemarkOpen && (initialPass || content != m_kmlContent)) { EndPlacemark(); @@ -177,7 +184,7 @@ } m_kmlContent = content; - if (!m_placemarkOpen) + if (initialPass || !m_placemarkOpen) { m_placemarkName.clear(); m_kmlContent->WriteString(""); Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 05:51:50 UTC (rev 10257) @@ -61,6 +61,8 @@ RENDERERS_API virtual void EndLayer(); + virtual void EndFeature(); + RENDERERS_API virtual void StartFeature(RS_FeatureReader* feature, bool initialPass, const RS_String* tooltip = NULL, Modified: branches/4.0/MgDev/Common/Stylization/Renderer.h =================================================================== --- branches/4.0/MgDev/Common/Stylization/Renderer.h 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Common/Stylization/Renderer.h 2026-11-03 05:51:50 UTC (rev 10257) @@ -76,11 +76,10 @@ /// /// Indicates the renderer should process all subsequent geometry - /// as part of a single logical feature. When a feature is stylized by - /// several composite type styles, this is called once per style and - /// initialPass is true only for the first of those calls, which is - /// where a renderer that groups a feature's output (the KML - /// placemark, for example) opens its group. + /// as part of a single logical feature. A feature is stylized once per + /// composite type style, so this is called once per style pass and + /// initialPass is set only for the first style of the feature's first + /// rendering pass; EndFeature is called when the last pass is done. /// virtual void StartFeature(RS_FeatureReader* feature, bool initialPass, @@ -92,11 +91,11 @@ RS_ElevationType zOffsetType = RS_ElevationType_RelativeToGround) = 0; /// - /// Done processing current feature. + /// Done processing current feature - every style pass of it has been + /// sent. Renderers that group a feature's output (the KML placemark, for + /// example) close their group here; the rest have nothing to do. /// - //TODO: is it needed -- next call to StartFeature can just imply - //previous feature is done - //virtual void EndFeature() = 0; + virtual void EndFeature() {} /// /// Polygon features. Transformation to W2D space is done here. Also Modified: branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp =================================================================== --- branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp 2026-11-03 05:51:50 UTC (rev 10257) @@ -166,22 +166,17 @@ } // stylize once for each composite type style - bool featureStarted = false; for (size_t i=0; iEndFeature(); + // free geometry when done stylizing LineBufferPool::FreeLineBuffer(m_pool, spLB.release()); @@ -759,7 +754,7 @@ } -bool StylizationEngine::Stylize(RS_FeatureReader* reader, +void StylizationEngine::Stylize(RS_FeatureReader* reader, FdoExpressionEngine* exec, LineBuffer* geometry, CompositeTypeStyle* style, @@ -832,9 +827,8 @@ } } - // no rule matches, so the renderer never hears about this feature if (rule == NULL) - return false; + return; // we found a valid rule - send a StartFeature notification RS_String rs_tip, rs_url; @@ -849,7 +843,7 @@ // up with no symbols - we're done in that case. std::vector* symbolInstances = &rule->symbolInstances; if (symbolInstances->size() == 0) - return true; + return; double mm2sud = m_serenderer->GetScreenUnitsPerMillimeterDevice(); double mm2suw = m_serenderer->GetScreenUnitsPerMillimeterWorld(); @@ -1112,7 +1106,7 @@ // if the clipped buffer is NULL (completely clipped) just move on to // the next feature if (!lbc) - return true; + return; // otherwise continue processing with the clipped buffer lb = lbc; @@ -1259,8 +1253,6 @@ // free clipped line buffer if the geometry was clipped if (spClipLB.get()) LineBufferPool::FreeLineBuffer(m_pool, spClipLB.release()); - - return true; } Modified: branches/4.0/MgDev/Common/Stylization/StylizationEngine.h =================================================================== --- branches/4.0/MgDev/Common/Stylization/StylizationEngine.h 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Common/Stylization/StylizationEngine.h 2026-11-03 05:51:50 UTC (rev 10257) @@ -78,9 +78,7 @@ int saveHeight); // Stylizes the current feature on the reader using the supplied composite type style. - // Returns true if the renderer was given the feature - the style's rules can match - // nothing, in which case no StartFeature() is issued for it. - bool Stylize(RS_FeatureReader* reader, + void Stylize(RS_FeatureReader* reader, FdoExpressionEngine* exec, LineBuffer* geometry, CompositeTypeStyle* style, Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 05:32:43 UTC (rev 10256) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestKmlService.cpp 2026-11-03 05:51:50 UTC (rev 10257) @@ -519,10 +519,10 @@ return content; } - // Renders one point feature, calling StartFeature() once per composite type - // style pass, in the order StylizationEngine does it. A feature whose leading - // styles match no rule has no call for those styles, so initialPass marks the - // first pass it actually reaches the renderer for. + // Renders one point feature: StartFeature() once per composite type style pass, + // in the order StylizationEngine does it, and then EndFeature(). initialPass is + // set only for the first style pass of the feature's first rendering pass, so a + // later rendering pass re-stylizes every feature with initialPass clear. // // ProcessMarker() rather than ProcessPolyline() because it is the one the // renderer exports: the geometry adapters reach the other Process* methods @@ -530,7 +530,7 @@ // when KmlRenderer lives in its own DLL (the Windows build). void StylizePointFeature(KmlRenderer& renderer, int stylePasses, - bool initialPassOnFirst, + bool firstRenderingPass, double y, const RS_String* theme = NULL) { @@ -541,19 +541,28 @@ for (int pass = 0; pass < stylePasses; ++pass) { - renderer.StartFeature(NULL, initialPassOnFirst && pass == 0, NULL, NULL, theme, 0.0, 0.0, RS_ElevationType_RelativeToGround); + renderer.StartFeature(NULL, firstRenderingPass && pass == 0, NULL, NULL, theme, 0.0, 0.0, RS_ElevationType_RelativeToGround); renderer.ProcessMarker(&lb, mdef, true, NULL); } + renderer.EndFeature(); } - void StartRailLayer(KmlRenderer& renderer) + // The renderer keeps a raw pointer to the layer and feature class info it is + // handed, so they have to outlive it for as long as it renders that layer. + struct RailLayer { - RS_UIGraphic graphic(NULL, 0, L""); - RS_LayerUIInfo layerInfo(L"Rail", L"Library://UnitTests/Layers/MultiCTS.LayerDefinition", - L"", true, true, false, L"", L"", true, true, 0.0, graphic); - RS_FeatureClassInfo classInfo(L"Rail", L"Library://UnitTests/Data/Rail.FeatureSource", L"SHPGEOM"); - renderer.StartLayer(&layerInfo, &classInfo); - } + RailLayer() + : graphic(NULL, 0, L"") + , layerInfo(L"Rail", L"Library://UnitTests/Layers/MultiCTS.LayerDefinition", + L"", true, true, false, L"", L"", true, true, 0.0, graphic) + , classInfo(L"Rail", L"Library://UnitTests/Data/Rail.FeatureSource", L"SHPGEOM") + { + } + + RS_UIGraphic graphic; + RS_LayerUIInfo layerInfo; + RS_FeatureClassInfo classInfo; + }; } TEST_CASE("KmlCompositeStyleWritesOnePlacemark", "[KmlService]") @@ -560,8 +569,9 @@ { KmlContent kmlContent; RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + RailLayer layer; KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); - StartRailLayer(renderer); + renderer.StartLayer(&layer.layerInfo, &layer.classInfo); // one feature, stylized by three composite type styles StylizePointFeature(renderer, 3, true, 43.75); @@ -580,8 +590,9 @@ { KmlContent kmlContent; RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + RailLayer layer; KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); - StartRailLayer(renderer); + renderer.StartLayer(&layer.layerInfo, &layer.classInfo); // two features, two composite type styles each StylizePointFeature(renderer, 2, true, 43.75); @@ -605,8 +616,9 @@ { KmlContent kmlContent; RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + RailLayer layer; KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); - StartRailLayer(renderer); + renderer.StartLayer(&layer.layerInfo, &layer.classInfo); // a feature whose two composite type styles carry different legend labels: // each theme is written into its own folder, and a placemark cannot be left @@ -613,9 +625,17 @@ // open across folders, so the second theme starts a placemark of its own RS_String themeA(L"Single Track"); RS_String themeB(L"Dual Track"); - StylizePointFeature(renderer, 1, true, 43.75, &themeA); - StylizePointFeature(renderer, 1, false, 43.75, &themeB); + LineBuffer lb(2); + lb.MoveTo(-87.70, 43.75); + RS_MarkerDef mdef; + + renderer.StartFeature(NULL, true, NULL, NULL, &themeA, 0.0, 0.0, RS_ElevationType_RelativeToGround); + renderer.ProcessMarker(&lb, mdef, true, NULL); + renderer.StartFeature(NULL, false, NULL, NULL, &themeB, 0.0, 0.0, RS_ElevationType_RelativeToGround); + renderer.ProcessMarker(&lb, mdef, true, NULL); + renderer.EndFeature(); + renderer.EndLayer(); std::string kml = kmlContent.GetString(); @@ -629,6 +649,32 @@ REQUIRE(GetContentOutsidePlacemarks(kml).find("") == std::string::npos); } +TEST_CASE("KmlCompositeStyleSeparatesFeaturesInLaterRenderingPass", "[KmlService]") +{ + KmlContent kmlContent; + RS_Bounds bounds(-87.8, 43.7, -87.6, 43.8); + RailLayer layer; + KmlRenderer renderer(&kmlContent, bounds, 10000.0, 96.0, 1.0, 0); + renderer.StartLayer(&layer.layerInfo, &layer.classInfo); + + // a symbol rendering pass re-stylizes every feature with initialPass clear, so + // nothing but EndFeature separates one feature from the next there + StylizePointFeature(renderer, 1, false, 43.75); + StylizePointFeature(renderer, 1, false, 43.78); + + renderer.EndLayer(); + + std::string kml = kmlContent.GetString(); + INFO(kml); + std::vector placemarks = GetPlacemarks(kml); + REQUIRE(placemarks.size() == 2); + for (size_t i = 0; i < placemarks.size(); ++i) + { + REQUIRE(CountOccurrences(placemarks[i], "") == 1); + } + REQUIRE(GetContentOutsidePlacemarks(kml).find("") == std::string::npos); +} + TEST_CASE("GetFeaturesKmlCompositeStylePerFeature", "[KmlService]") { try From svn_mapguide at osgeo.org Tue Nov 3 02:35:21 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 02:35:21 -0800 Subject: [mapguide-commits] r10258 - branches/4.0/MgDev/Common/Renderers Message-ID: <20261103103521.D3DB31D5E47@trac.osgeo.org> Author: jng Date: 2026-11-03 02:35:20 -0800 (Tue, 03 Nov 2026) New Revision: 10258 Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h Log: Fix windows build Modified: branches/4.0/MgDev/Common/Renderers/KmlRenderer.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 05:51:50 UTC (rev 10257) +++ branches/4.0/MgDev/Common/Renderers/KmlRenderer.h 2026-11-03 10:35:20 UTC (rev 10258) @@ -61,7 +61,7 @@ RENDERERS_API virtual void EndLayer(); - virtual void EndFeature(); + RENDERERS_API virtual void EndFeature(); RENDERERS_API virtual void StartFeature(RS_FeatureReader* feature, bool initialPass, From svn_mapguide at osgeo.org Tue Nov 3 03:44:16 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 03:44:16 -0800 Subject: [mapguide-commits] r10259 - in branches/4.0/MgDev: Common/Renderers Server/src/UnitTesting Message-ID: <20261103114416.5AB8A41021@trac.osgeo.org> Author: jng Date: 2026-11-03 03:44:14 -0800 (Tue, 03 Nov 2026) New Revision: 10259 Modified: branches/4.0/MgDev/Common/Renderers/AGGRenderer.cpp branches/4.0/MgDev/Common/Renderers/agg_context.h branches/4.0/MgDev/Server/src/UnitTesting/TestRenderingService.cpp Log: Fix area clip settings not being honored. Fixes #2572 Modified: branches/4.0/MgDev/Common/Renderers/AGGRenderer.cpp =================================================================== --- branches/4.0/MgDev/Common/Renderers/AGGRenderer.cpp 2026-11-03 10:35:20 UTC (rev 10258) +++ branches/4.0/MgDev/Common/Renderers/AGGRenderer.cpp 2026-11-03 11:44:14 UTC (rev 10259) @@ -1475,6 +1475,53 @@ ////////////////////////////////////////////////////////////////////////////// +// Draws the glyphs of a string using the supplied scanline renderer. +template +static void DrawStringGlyphs(agg_context* cxt, unsigned int* p, double& x, double& y, + agg::glyph_rendering glyph_type, RendererType& ren) +{ + if (glyph_type == agg::glyph_ren_agg_gray8) + { + while (*p) + { + const agg::glyph_cache* glyph = cxt->fman.glyph(*p++); + cxt->fman.add_kerning(&x, &y); + cxt->fman.init_embedded_adaptors(glyph, x, y); + + agg::render_scanlines(cxt->fman.gray8_adaptor(), cxt->fman.gray8_scanline(), ren); + + x += glyph->advance_x; + y += glyph->advance_y; + } + } + else + { + font_curve_type curves(cxt->fman.path_adaptor()); + font_contour_type contour(curves); + + cxt->ras.filling_rule(agg::fill_non_zero); + + while (*p) + { + const agg::glyph_cache* glyph = cxt->fman.glyph(*p++); + cxt->fman.add_kerning(&x, &y); + cxt->fman.init_embedded_adaptors(glyph, x, y); + + cxt->ras.reset(); + cxt->ras.add_path(contour); + + agg::render_scanlines(cxt->ras, cxt->sl, ren); + + x += glyph->advance_x; + y += glyph->advance_y; + } + + cxt->ras.filling_rule(agg::fill_even_odd); + } +} + + +////////////////////////////////////////////////////////////////////////////// void AGGRenderer::DrawString(agg_context* cxt, const RS_String& s, double x, @@ -1552,9 +1599,6 @@ cxt->last_font_transform = mtx; } - mg_ren_aa_solid ren_aa_solid(cxt->ren); - ren_aa_solid.color(agg::argb8_packed(color.argb())); - unsigned int* text; #ifdef _WIN32 // TODO: check if we really need to convert UCS-2 to UCS-4 on Windows or @@ -1566,47 +1610,20 @@ #endif unsigned int* p = text; - if (glyph_type == agg::glyph_ren_agg_gray8) + if (cxt->bPolyClip) { - while (*p) - { - const agg::glyph_cache* glyph = cxt->fman.glyph(*p++); - cxt->fman.add_kerning(&x, &y); - cxt->fman.init_embedded_adaptors(glyph, x, y); - - agg::render_scanlines(cxt->fman.gray8_adaptor(), cxt->fman.gray8_scanline(), ren_aa_solid); - - x += glyph->advance_x; - y += glyph->advance_y; - } + mg_clip_mask_ren_aa_solid clip_ren_aa_solid(cxt->clip_rb); + clip_ren_aa_solid.color(agg::argb8_packed(color.argb())); + DrawStringGlyphs(cxt, p, x, y, glyph_type, clip_ren_aa_solid); } else { - font_curve_type curves(cxt->fman.path_adaptor()); - font_contour_type contour(curves); - - cxt->ras.filling_rule(agg::fill_non_zero); - - while (*p) - { - const agg::glyph_cache* glyph = cxt->fman.glyph(*p++); - cxt->fman.add_kerning(&x, &y); - cxt->fman.init_embedded_adaptors(glyph, x, y); - - cxt->ras.reset(); - cxt->ras.add_path(contour); - - agg::render_scanlines(cxt->ras, cxt->sl, ren_aa_solid); - - x += glyph->advance_x; - y += glyph->advance_y; - } - - cxt->ras.filling_rule(agg::fill_even_odd); + mg_ren_aa_solid ren_aa_solid(cxt->ren); + ren_aa_solid.color(agg::argb8_packed(color.argb())); + DrawStringGlyphs(cxt, p, x, y, glyph_type, ren_aa_solid); } } - ////////////////////////////////////////////////////////////////////////////// void AGGRenderer::MeasureString(const RS_String& s, double height, @@ -1818,6 +1835,7 @@ // this is important because we will only clear that section of the alpha mask // buffer and the rest may be dirty c()->clip_rb.clip_box(iminx, iminy, imaxx, imaxy); + c()->clip_rb_pre.clip_box(iminx, iminy, imaxx, imaxy); // transform to screen coords and transfer to AGG storage unsigned int* pathids = (unsigned int*)alloca(polygon->geom_count() * sizeof(unsigned int)); @@ -2671,6 +2689,18 @@ ////////////////////////////////////////////////////////////////////////////// // Renders the source image to the destination context, using the specified // affine transformation matrix. +template +static void RenderImageSpans(agg_context* cxt, RendererType& ren, + SpanAllocatorType& sa, SpanGeneratorType& sg, + bool antiAlias) +{ + if (antiAlias) + agg::render_scanlines_aa(cxt->ras, cxt->sl, ren, sa, sg); + else + agg::render_scanlines_bin(cxt->ras, cxt->sl, ren, sa, sg); +} + + void AGGRenderer::RenderWithTransform(mg_rendering_buffer& src, agg_context* cxt, agg::trans_affine& img_mtx, RS_ImageFormat format, bool antiAlias) { @@ -2690,10 +2720,10 @@ agg::span_allocator sa; - if (antiAlias) - agg::render_scanlines_aa(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + if (cxt->bPolyClip) + RenderImageSpans(cxt, cxt->clip_rb_pre, sa, sg, antiAlias); else - agg::render_scanlines_bin(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + RenderImageSpans(cxt, cxt->ren_pre, sa, sg, antiAlias); } else if (format == RS_ImageFormat_ARGB) { @@ -2712,10 +2742,10 @@ agg::span_allocator sa; // we are using the alpha premultiplied renderer since the source image is premultiplied - if (antiAlias) - agg::render_scanlines_aa(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + if (cxt->bPolyClip) + RenderImageSpans(cxt, cxt->clip_rb_pre, sa, sg, antiAlias); else - agg::render_scanlines_bin(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + RenderImageSpans(cxt, cxt->ren_pre, sa, sg, antiAlias); } else if (format == RS_ImageFormat_NATIVE) { @@ -2734,10 +2764,10 @@ agg::span_allocator sa; - if (antiAlias) - agg::render_scanlines_aa(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + if (cxt->bPolyClip) + RenderImageSpans(cxt, cxt->clip_rb_pre, sa, sg, antiAlias); else - agg::render_scanlines_bin(cxt->ras, cxt->sl, cxt->ren_pre, sa, sg); + RenderImageSpans(cxt, cxt->ren_pre, sa, sg, antiAlias); } } Modified: branches/4.0/MgDev/Common/Renderers/agg_context.h =================================================================== --- branches/4.0/MgDev/Common/Renderers/agg_context.h 2026-11-03 10:35:20 UTC (rev 10258) +++ branches/4.0/MgDev/Common/Renderers/agg_context.h 2026-11-03 11:44:14 UTC (rev 10259) @@ -103,6 +103,9 @@ typedef agg::renderer_base mg_clip_mask_ren_base; typedef agg::renderer_scanline_aa_solid mg_clip_mask_ren_aa_solid; +// premultiplied variant, used to clip images and other premultiplied spans +typedef agg::pixfmt_amask_adaptor mg_pixfmt_clip_mask_type_pre; +typedef agg::renderer_base mg_clip_mask_ren_base_pre; struct RS_Font; @@ -154,6 +157,9 @@ clip_mask = new mg_pixfmt_clip_mask_type(clip_pixf, mask_mask); clip_rb.attach(*clip_mask); clip_ren.attach(clip_rb); + + clip_mask_pre = new mg_pixfmt_clip_mask_type_pre(pf_pre, mask_mask); + clip_rb_pre.attach(*clip_mask_pre); } ~agg_context() @@ -161,6 +167,7 @@ if (ownrows) delete [] m_rows; + delete clip_mask_pre; delete clip_mask; delete mask_pixf; delete [] mask_buf; @@ -193,6 +200,10 @@ mg_pixfmt_type clip_pixf; mg_pixfmt_clip_mask_type* clip_mask; + // premultiplied alpha-masked renderer, for image spans + mg_clip_mask_ren_base_pre clip_rb_pre; + mg_pixfmt_clip_mask_type_pre* clip_mask_pre; + // font engine state caching font_engine_type feng; font_manager_type fman; Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestRenderingService.cpp =================================================================== --- branches/4.0/MgDev/Server/src/UnitTesting/TestRenderingService.cpp 2026-11-03 10:35:20 UTC (rev 10258) +++ branches/4.0/MgDev/Server/src/UnitTesting/TestRenderingService.cpp 2026-11-03 11:44:14 UTC (rev 10259) @@ -24,10 +24,13 @@ //#include "AGGRenderer.h" #include "FoundationDefs.h" #include "SE_Renderer.h" +#include "AGGImageIO.h" #include "TestServiceFactory.h" #include "catch.hpp" +#include + static MgMap* CreateTestMapWithWatermark() { Ptr svcSite = TestServiceFactory::CreateSiteService(); @@ -2057,4 +2060,293 @@ { throw; } +} + + +///---------------------------------------------------------------------------- +/// Test Case Description: +/// +/// Renders a polygon whose area style is a custom symbol that tiles an image and +/// a text primitive across the polygon's bounding box. The symbol's clipping +/// control is Clip, so nothing the symbol draws may appear outside the polygon. +/// The polygon covers the left half of the rendered image, which leaves the +/// right half free of it; the test asserts the right half contains neither the +/// image color nor the text color. +/// +/// The ticket this covers is +/// https://trac.osgeo.org/mapguide/ticket/2572 - an area usage symbol with an +/// image used to draw an unclipped rectangle over the map. +///---------------------------------------------------------------------------- +TEST_CASE("RenderAreaSymbolClipping", "[RenderingService]") +{ + try + { + // Set the user information for the current thread to be administrator. + Ptr adminUserInfo = new MgUserInformation(MgUser::Administrator, L""); + MgUserInformation::SetCurrentUserInfo(adminUserInfo); + + MgServiceManager* serviceManager = MgServiceManager::GetInstance(); + if (serviceManager == NULL) + throw new MgException(MgExceptionCodes::MgNullReferenceException, L"TestRenderingService.RenderAreaSymbolClipping", __LINE__, __WFILE__, NULL, L"", NULL); + + Ptr svcFeature = dynamic_cast(serviceManager->RequestService(MgServiceType::FeatureService)); + if (svcFeature == NULL) + throw new MgException(MgExceptionCodes::MgServiceNotAvailableException, L"TestRenderingService.RenderAreaSymbolClipping", __LINE__, __WFILE__, NULL, L"", NULL); + + Ptr svcResource = TestServiceFactory::CreateResourceService(); + + Ptr fsId = new MgResourceIdentifier(L"Library://UnitTests/Data/AreaSymbolClippingTest.FeatureSource"); + Ptr ldfId = new MgResourceIdentifier(L"Library://UnitTests/Layers/AreaSymbolClippingTest.LayerDefinition"); + Ptr mdfId = new MgResourceIdentifier(L"Library://UnitTests/Maps/AreaSymbolClippingTest.MapDefinition"); + + // Drop anything an earlier run left behind. + if (svcResource->ResourceExists(mdfId)) svcResource->DeleteResource(mdfId); + if (svcResource->ResourceExists(ldfId)) svcResource->DeleteResource(ldfId); + if (svcResource->ResourceExists(fsId)) svcResource->DeleteResource(fsId); + + Ptr csFactory = new MgCoordinateSystemFactory(); + STRING csWkt = csFactory->ConvertCoordinateSystemCodeToWkt(L"LL84"); + std::string csWktMb = MgUtil::WideCharToMultiByte(csWkt); + + STRING scName = L"Default"; + + Ptr schema = new MgFeatureSchema(L"Default", L"Area symbol clipping test schema"); + Ptr klass = new MgClassDefinition(); + klass->SetName(L"TestClass"); + + Ptr clsProps = klass->GetProperties(); + Ptr clsIdProps = klass->GetIdentityProperties(); + + Ptr id = new MgDataPropertyDefinition(L"ID"); + id->SetDataType(MgPropertyType::Int32); + id->SetAutoGeneration(true); + clsProps->Add(id); + clsIdProps->Add(id); + + Ptr geom = new MgGeometricPropertyDefinition(L"Geom"); + geom->SetGeometryTypes(MgFeatureGeometricType::Surface); + geom->SetSpatialContextAssociation(scName); + clsProps->Add(geom); + + klass->SetDefaultGeometryPropertyName(L"Geom"); + + Ptr classes = schema->GetClasses(); + classes->Add(klass); + + Ptr fsParams = new MgFileFeatureSourceParams(L"OSGeo.SDF", scName, csWkt, schema); + svcFeature->CreateFeatureSource(fsId, fsParams); + + // The left half of the rendered image. Everything to the right of x = 0 + // is outside the polygon. + MgGeometryFactory geomFactory; + + Ptr ringCoords = new MgCoordinateCollection(); + Ptr pt = geomFactory.CreateCoordinateXY(-1.0, -1.0); + ringCoords->Add(pt); + pt = geomFactory.CreateCoordinateXY(0.0, -1.0); + ringCoords->Add(pt); + pt = geomFactory.CreateCoordinateXY(0.0, 1.0); + ringCoords->Add(pt); + pt = geomFactory.CreateCoordinateXY(-1.0, 1.0); + ringCoords->Add(pt); + pt = geomFactory.CreateCoordinateXY(-1.0, -1.0); + ringCoords->Add(pt); + + Ptr ring = geomFactory.CreateLinearRing(ringCoords); + Ptr polygon = geomFactory.CreatePolygon(ring, nullptr); + + Ptr agfRw = new MgAgfReaderWriter(); + Ptr agf = agfRw->Write(polygon); + + Ptr propVals = new MgPropertyCollection(); + Ptr geomVal = new MgGeometryProperty(L"Geom", agf); + propVals->Add(geomVal); + + Ptr fr = svcFeature->InsertFeatures(fsId, L"Default:TestClass", propVals); + fr->Close(); + + // An 8x8 opaque red PNG, embedded in the symbol definition. It is drawn + // 100 mm wide, which is large enough that an unclipped copy covers a + // substantial part of the rendered image. + const char* pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAUSURBVChTY/jPwPAfHx4RChj+AwDC13+BykaqugAAAABJRU5ErkJggg=="; + + std::string ldfXml; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += "Library://UnitTests/Data/AreaSymbolClippingTest.FeatureSource"; + ldfXml += ""; + ldfXml += "Default:TestClass"; + ldfXml += "FeatureClass"; + ldfXml += "Geom"; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += "Area Clipping Symbol"; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += pngBase64; + ldfXml += ""; + ldfXml += "100"; + ldfXml += "100"; + ldfXml += "false"; + ldfXml += ""; + ldfXml += ""; + ldfXml += "'CLIP'"; + ldfXml += "'Arial'"; + ldfXml += "30"; + ldfXml += "false"; + ldfXml += "ff00ff00"; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += "'FromAngle'"; + ldfXml += "'Global'"; + ldfXml += "'Clip'"; + ldfXml += "0"; + ldfXml += "0"; + ldfXml += "0"; + ldfXml += "100"; + ldfXml += "100"; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += "DeviceUnits"; + ldfXml += "Polygon"; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + ldfXml += ""; + + Ptr ldfBs = new MgByteSource((BYTE_ARRAY_IN)ldfXml.c_str(), (INT32)ldfXml.length()); + Ptr ldfContent = ldfBs->GetReader(); + svcResource->SetResource(ldfId, ldfContent, nullptr); + + std::string mdfXml; + mdfXml += ""; + mdfXml += ""; + mdfXml += "AreaSymbolClippingTest"; + mdfXml += ""; + mdfXml += csWktMb; + mdfXml += ""; + mdfXml += "-11-11"; + mdfXml += "ffffffff"; + mdfXml += ""; + mdfXml += "AreaSymbolClipping"; + mdfXml += "Library://UnitTests/Layers/AreaSymbolClippingTest.LayerDefinition"; + mdfXml += "true"; + mdfXml += "true"; + mdfXml += "AreaSymbolClipping"; + mdfXml += "true"; + mdfXml += "true"; + mdfXml += ""; + mdfXml += ""; + mdfXml += ""; + mdfXml += ""; + + Ptr mdfBs = new MgByteSource((BYTE_ARRAY_IN)mdfXml.c_str(), (INT32)mdfXml.length()); + Ptr mdfContent = mdfBs->GetReader(); + svcResource->SetResource(mdfId, mdfContent, nullptr); + + Ptr svcSite = TestServiceFactory::CreateSiteService(); + Ptr siteConnection = TestServiceFactory::CreateSiteConnection(svcSite); + + Ptr map = new MgMap(siteConnection); + map->Create(mdfId, mdfId->GetName()); + map->SetDisplayDpi(96); + map->SetDisplayWidth(512); + map->SetDisplayHeight(512); + + // Render exactly the extents the polygon is defined against, so it + // covers the left half of the image. + const INT32 imageSize = 512; + Ptr extents = new MgEnvelope(-1.0, -1.0, 1.0, 1.0); + Ptr bgColor = new MgColor(255, 255, 255, 255); + + Ptr svcRendering = TestServiceFactory::CreateRenderingService(); + Ptr png = svcRendering->RenderMap(map, nullptr, extents, imageSize, imageSize, bgColor, MgImageFormats::Png); + png->ToFile(L"../UnitTestFiles/Results/RenderAreaSymbolClipping.png"); + + Ptr sink = new MgByteSink(png); + Ptr bytes = sink->ToBuffer(); + REQUIRE(bytes->GetLength() > 0); + + int width = 0; + int height = 0; + unsigned int* decoded = AGGImageIO::DecodePNG(bytes->Bytes(), (size_t)bytes->GetLength(), width, height); + REQUIRE(decoded != NULL); + // DecodePNG hands back a buffer that has to be released with delete[]. + std::unique_ptr pixels(decoded); + REQUIRE(width == imageSize); + REQUIRE(height == imageSize); + + // Count the image and text colors inside and to the right of the polygon. + // DecodePNG returns the image bottom up. + int redInside = 0, redOutside = 0; + int greenInside = 0, greenOutside = 0; + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + unsigned int argb = decoded[(height - 1 - y) * width + x]; + int r = (int)((argb >> 16) & 0xFF); + int g = (int)((argb >> 8) & 0xFF); + int b = (int)(argb & 0xFF); + + bool isRed = (r > 200 && g < 60 && b < 60); + bool isGreen = (g > 200 && r < 60 && b < 60); + + // The polygon's right edge is at x = width / 2. Look a quarter + // of the image further right to stay clear of the boundary's + // antialiasing. + bool outside = (x > width * 3 / 4); + + if (isRed) + outside ? ++redOutside : ++redInside; + if (isGreen) + outside ? ++greenOutside : ++greenInside; + } + } + + INFO("red pixels inside=" << redInside << " outside=" << redOutside + << ", green pixels inside=" << greenInside << " outside=" << greenOutside); + + // The symbol must actually be drawing, otherwise the clipping checks + // below would pass for the wrong reason. + CHECK(redInside > 1000); + CHECK(greenInside > 0); + + // Nothing the area symbol draws may leak outside the polygon. + CHECK(redOutside == 0); + CHECK(greenOutside == 0); + + // Leave the repository as we found it. + svcResource->DeleteResource(mdfId); + svcResource->DeleteResource(ldfId); + svcResource->DeleteResource(fsId); + } + catch (MgException* e) + { + STRING message = e->GetDetails(TestServiceFactory::TEST_LOCALE); + SAFE_RELEASE(e); + FAIL(MG_WCHAR_TO_CHAR(message.c_str())); + } + catch (FdoException* e) + { + FDO_SAFE_RELEASE(e); + FAIL("FdoException occurred"); + } + catch (...) + { + throw; + } } \ No newline at end of file From svn_mapguide at osgeo.org Tue Nov 3 03:46:52 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 03:46:52 -0800 Subject: [mapguide-commits] r10260 - branches/4.0/MgDev Message-ID: <20261103114652.513984342C@trac.osgeo.org> Author: jng Date: 2026-11-03 03:46:51 -0800 (Tue, 03 Nov 2026) New Revision: 10260 Modified: branches/4.0/MgDev/build.bat Log: Prepare staging area Repositories dir instead of copying the source one Modified: branches/4.0/MgDev/build.bat =================================================================== --- branches/4.0/MgDev/build.bat 2026-11-03 11:44:14 UTC (rev 10259) +++ branches/4.0/MgDev/build.bat 2026-11-03 11:46:51 UTC (rev 10260) @@ -293,18 +293,28 @@ %XCOPY_SINGLE% "%MG_SERVER%\bin\%TYPEBUILD%" "%MG_OUTPUT_SERVER%\bin" /EXCLUDE:%CONFIGURATION%_excludes.txt echo [install]: Server - FDO %XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\FDO" "%MG_OUTPUT_SERVER%\FDO" /EXCLUDE:%CONFIGURATION%_excludes.txt -echo [install]: Server - Logs -%XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Logs" "%MG_OUTPUT_SERVER%\Logs" -echo [install]: Server - Packages -%XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Packages" "%MG_OUTPUT_SERVER%\Packages" echo [install]: Server - Repositories -%XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Repositories" "%MG_OUTPUT_SERVER%\Repositories" +rem Repositories, Logs, Packages and Temp are runtime state: the test suites +rem and any server started from the build output write into them, so copying +rem them out of the build output ships that state with the installation. An +rem installation has them empty - the server initializes its repositories on +rem its first start - so create the empty layout instead of copying it. +if exist "%MG_OUTPUT_SERVER%\Repositories" rd /S /Q "%MG_OUTPUT_SERVER%\Repositories" +if exist "%MG_OUTPUT_SERVER%\Logs" rd /S /Q "%MG_OUTPUT_SERVER%\Logs" +if exist "%MG_OUTPUT_SERVER%\Packages" rd /S /Q "%MG_OUTPUT_SERVER%\Packages" +if exist "%MG_OUTPUT_SERVER%\Temp" rd /S /Q "%MG_OUTPUT_SERVER%\Temp" +mkdir "%MG_OUTPUT_SERVER%\Repositories\Library\DataFiles" +mkdir "%MG_OUTPUT_SERVER%\Repositories\Session\DataFiles" +mkdir "%MG_OUTPUT_SERVER%\Repositories\Site" +mkdir "%MG_OUTPUT_SERVER%\Repositories\TileCache" +mkdir "%MG_OUTPUT_SERVER%\Logs" +mkdir "%MG_OUTPUT_SERVER%\Packages" +mkdir "%MG_OUTPUT_SERVER%\Temp" +if not exist "%MG_OUTPUT_SERVER%\Repositories\TileCache" goto error_create_runtime_folders echo [install]: Server - Resources %XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Resources" "%MG_OUTPUT_SERVER%\Resources" echo [install]: Server - Schema %XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Schema" "%MG_OUTPUT_SERVER%\Schema" -echo [install]: Server - Temp -%XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\Temp" "%MG_OUTPUT_SERVER%\Temp" echo [install]: Server - WFS %XCOPY% "%MG_SERVER%\bin\%TYPEBUILD%\wfs" "%MG_OUTPUT_SERVER%\wfs" echo [install]: Server - WMS @@ -496,6 +506,10 @@ copy /Y "%CD%\Bindings\src\ApiDump\README.txt" "%MG_OUTPUT%\Snapshot" goto quit +:error_create_runtime_folders +SET ERRORMSG=Could not create the empty server runtime folders under %MG_OUTPUT_SERVER% +goto custom_error_no_help + :error echo [ERROR]: There was an error building the component popd From svn_mapguide at osgeo.org Tue Nov 3 04:22:57 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 04:22:57 -0800 Subject: [mapguide-commits] r10261 - in branches/4.0/MgDev: Common/MapGuideCommon/Util Portable/MgPortable/System Message-ID: <20261103122257.F3D0744C7A@trac.osgeo.org> Author: jng Date: 2026-11-03 04:22:56 -0800 (Tue, 03 Nov 2026) New Revision: 10261 Modified: branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.cpp branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.h branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.cpp branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.h Log: Fix concurrent render timing measurements Use a per-call counter in the server and portable timer utilities to prevent concurrent calls from overwriting one another's samples. Initialize the performance-counter frequency safely, and report counter-query failures. Fixes #2302 Modified: branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.cpp =================================================================== --- branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.cpp 2026-11-03 11:46:51 UTC (rev 10260) +++ branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.cpp 2026-11-03 12:22:56 UTC (rev 10261) @@ -18,13 +18,6 @@ #include "MapGuideCommon.h" #include "TimerUtil.h" -#ifdef WIN32 -LARGE_INTEGER MgTimerUtil::frequency = {0}; -LARGE_INTEGER MgTimerUtil::count = {0}; -#else -timeval MgTimerUtil::count = {0}; -#endif - ///---------------------------------------------------------------------------- /// /// Constructs the object. @@ -54,14 +47,20 @@ double MgTimerUtil::GetTime() { #ifdef WIN32 - if(0 == frequency.QuadPart) + static const LARGE_INTEGER frequency = []() { - GetFrequency(&frequency); - } - QueryPerformanceCounter(&count); + LARGE_INTEGER value; + GetFrequency(&value); + return value; + }(); - return count.QuadPart* (1000.0 / frequency.QuadPart); + LARGE_INTEGER count; + if (!QueryPerformanceCounter(&count)) + throw new MgException(MgExceptionCodes::MgUnclassifiedException, L"MgTimerUtil.GetTime", __LINE__, __WFILE__, NULL, L"", NULL); + + return count.QuadPart * (1000.0 / frequency.QuadPart); #else + timeval count; gettimeofday(&count, NULL); return (count.tv_sec * 1000.0) + (count.tv_usec / 1000.0); #endif Modified: branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.h =================================================================== --- branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.h 2026-11-03 11:46:51 UTC (rev 10260) +++ branches/4.0/MgDev/Common/MapGuideCommon/Util/TimerUtil.h 2026-11-03 12:22:56 UTC (rev 10261) @@ -77,15 +77,6 @@ static void GetFrequency(LARGE_INTEGER* pFrequency); #endif - -private: - -#ifdef WIN32 - static LARGE_INTEGER frequency; - static LARGE_INTEGER count; -#else - static timeval count; -#endif }; /// \endcond Modified: branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.cpp 2026-11-03 11:46:51 UTC (rev 10260) +++ branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.cpp 2026-11-03 12:22:56 UTC (rev 10261) @@ -19,13 +19,6 @@ #include "MgPortable.h" #include "TimerUtil.h" -#ifdef WIN32 -LARGE_INTEGER MgPortableTimerUtil::frequency = {0}; -LARGE_INTEGER MgPortableTimerUtil::count = {0}; -#else -timeval MgPortableTimerUtil::count = {0}; -#endif - ///---------------------------------------------------------------------------- /// /// Constructs the object. @@ -55,14 +48,20 @@ double MgPortableTimerUtil::GetTime() { #ifdef WIN32 - if(0 == frequency.QuadPart) + static const LARGE_INTEGER frequency = []() { - GetFrequency(&frequency); - } - QueryPerformanceCounter(&count); + LARGE_INTEGER value; + GetFrequency(&value); + return value; + }(); - return count.QuadPart* (1000.0 / frequency.QuadPart); + LARGE_INTEGER count; + if (!QueryPerformanceCounter(&count)) + throw new MgException(MgExceptionCodes::MgUnclassifiedException, L"MgPortableTimerUtil.GetTime", __LINE__, __WFILE__, NULL, L"", NULL); + + return count.QuadPart * (1000.0 / frequency.QuadPart); #else + timeval count; gettimeofday(&count, NULL); return (count.tv_sec * 1000.0) + (count.tv_usec / 1000.0); #endif Modified: branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.h =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.h 2026-11-03 11:46:51 UTC (rev 10260) +++ branches/4.0/MgDev/Portable/MgPortable/System/TimerUtil.h 2026-11-03 12:22:56 UTC (rev 10261) @@ -77,15 +77,6 @@ static void GetFrequency(LARGE_INTEGER* pFrequency); #endif - -private: - -#ifdef WIN32 - static LARGE_INTEGER frequency; - static LARGE_INTEGER count; -#else - static timeval count; -#endif }; /// \endcond From svn_mapguide at osgeo.org Tue Nov 3 05:53:49 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 05:53:49 -0800 Subject: [mapguide-commits] r10262 - in branches/4.0/MgDev: . Bindings Bindings/src/Bindings Bindings/src/Bindings/DotNet Bindings/src/Bindings/DotNet/Portable Common Oem Portable Portable/DotNetUnitTest Portable/MgPortable Portable/MgPortable/Services/Rendering Portable/UnitTest Server/src/Gws Server/src/Gws/GwsCommon Server/src/Gws/GwsQueryEngine cmake/configs cmake/modules Message-ID: <20261103135349.B84391419FF@trac.osgeo.org> Author: jng Date: 2026-11-03 05:53:48 -0800 (Tue, 03 Nov 2026) New Revision: 10262 Added: branches/4.0/MgDev/cmake/configs/Platform.ini.in Modified: branches/4.0/MgDev/Bindings/setup_linux_native_libs.cmd branches/4.0/MgDev/Bindings/src/Bindings/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/CMakeLists.txt branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt branches/4.0/MgDev/CMakeLists.txt branches/4.0/MgDev/Common/CMakeLists.txt branches/4.0/MgDev/Oem/CMakeLists.txt branches/4.0/MgDev/Portable/CMakeLists.txt branches/4.0/MgDev/Portable/DotNetUnitTest/Program.cs branches/4.0/MgDev/Portable/DotNetUnitTest/ResourceServiceTests.cs branches/4.0/MgDev/Portable/MgPortable/CMakeLists.txt branches/4.0/MgDev/Portable/MgPortable/Services/Rendering/MappingUtil.cpp branches/4.0/MgDev/Portable/UnitTest/CMakeLists.txt branches/4.0/MgDev/Portable/UnitTest/TestFeatureService.cpp branches/4.0/MgDev/Portable/UnitTest/TestPerformance.cpp branches/4.0/MgDev/Portable/UnitTest/TestResourceService.cpp branches/4.0/MgDev/Portable/UnitTest/TestServiceFactory.cpp branches/4.0/MgDev/Portable/UnitTest/main.cpp branches/4.0/MgDev/Server/src/Gws/CMakeLists.txt branches/4.0/MgDev/Server/src/Gws/GwsCommon/CMakeLists.txt branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/CMakeLists.txt branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFlatFdoReader.cpp branches/4.0/MgDev/cmake/modules/FindMapGuideThirdparty.cmake branches/4.0/MgDev/cmake_bootstrap.sh branches/4.0/MgDev/cmake_build.sh Log: #2897: Add Linux build support for MapGuide Portable Modified: branches/4.0/MgDev/Bindings/setup_linux_native_libs.cmd =================================================================== --- branches/4.0/MgDev/Bindings/setup_linux_native_libs.cmd 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Bindings/setup_linux_native_libs.cmd 2026-11-03 13:53:48 UTC (rev 10262) @@ -1,4 +1,5 @@ SET TARBALL=%1 +SET PORTABLE_TARBALL=%2 SET SOVER=4.0.0 if "%TARBALL%"=="" goto :NoTarball if not exist "%TARBALL%" goto :NoTarball @@ -27,7 +28,25 @@ copy /Y /L "%~p0\linux-x64\lib64\libMgWebSupport-%SOVER%.so" "%~p0\src\Managed\DotNet\OSGeo.MapGuide.Web\runtimes\linux-x64\native" copy /Y /L "%~p0\linux-x64\lib64\libMgHttpHandler-%SOVER%.so" "%~p0\src\Managed\DotNet\OSGeo.MapGuide.Web\runtimes\linux-x64\native" copy /Y /L "%~p0\linux-x64\lib64\liblib_json.so" "%~p0\src\Managed\DotNet\OSGeo.MapGuide.Web\runtimes\linux-x64\native" +rem MgPortable setup. The portable tarball is optional; it carries MgPortable, its SWIG glue and +rem the GWS/Renderers/Stylization/Security libraries the portable API needs. +if "%PORTABLE_TARBALL%"=="" goto :Done +if not exist "%PORTABLE_TARBALL%" goto :NoPortableTarball +"%CD%\..\BuildTools\WebTools\7-Zip\7z.exe" x %PORTABLE_TARBALL% -so | "%CD%\..\BuildTools\WebTools\7-Zip\7z.exe" 7z x -aoa -si -ttar -o"%~p0\linux-x64" +set PORTABLE_NATIVE=%~p0\src\Managed\DotNet\OSGeo.MapGuide.Portable\runtimes\linux-x64\native +copy /Y /L "%~p0\linux-x64\lib64\libPortableUnmanagedApi.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgPortable-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgMdfModel-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgMdfParser-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgRenderers-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgStylization-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgGwsCommon-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgGwsQueryEngine-%SOVER%.so" "%PORTABLE_NATIVE%" +copy /Y /L "%~p0\linux-x64\lib64\libMgGwsResource-%SOVER%.so" "%PORTABLE_NATIVE%" goto :Done +:NoPortableTarball +echo No MgPortable linux native libs tarball specified or tarball does not exist. +goto :Done :NoTarball echo No linux native libs tarball specified or tarball does not exist. :Done \ No newline at end of file Modified: branches/4.0/MgDev/Bindings/src/Bindings/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Bindings/src/Bindings/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -12,13 +12,15 @@ ${XERCESC_INCLUDE_DIR} ) -# We only need to build the .net SWIG glue libs if building for the common subset -# We don't build this in a regular full end-to-end MG build -if (MG_COMMON_SUBSET_ONLY) +# The .net SWIG glue is only needed for the common subset, or for MgPortable (whose glue is the +# portable API). A regular full end-to-end MG build does not build it otherwise. +if (MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) add_subdirectory(DotNet) -else (MG_COMMON_SUBSET_ONLY) +endif (MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) + +if (NOT MG_COMMON_SUBSET_ONLY) if (WITH_JAVA) add_subdirectory(Java) endif (WITH_JAVA) add_subdirectory(Php) -endif (MG_COMMON_SUBSET_ONLY) \ No newline at end of file +endif (NOT MG_COMMON_SUBSET_ONLY) \ No newline at end of file Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -1,7 +1,17 @@ +file(COPY "dotnetcore_split.i" DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +# PortableApiGen.xml %imports the Foundation, Geometry and PlatformBase glue, so those three are +# always generated. The MapGuideCommon and Web glue is only needed by the common-libs subset +# (the published .NET packages); a full build with WITH_PORTABLE only needs the portable glue. add_subdirectory(Foundation) add_subdirectory(Geometry) add_subdirectory(PlatformBase) -add_subdirectory(MapGuideCommon) -add_subdirectory(Web) -file(COPY "dotnetcore_split.i" DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) \ No newline at end of file +if (MG_COMMON_SUBSET_ONLY) + add_subdirectory(MapGuideCommon) + add_subdirectory(Web) +endif (MG_COMMON_SUBSET_ONLY) + +if (WITH_PORTABLE) + add_subdirectory(Portable) +endif (WITH_PORTABLE) \ No newline at end of file Modified: branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Bindings/src/Bindings/DotNet/Portable/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -1,5 +1,16 @@ project(PortableUnmanagedApi) +# MgPortable.h (and the services it includes) pull in the public DWF Toolkit headers. +include_directories( + ${MG_PORTABLE_DIR}/MgPortable + ${MG_OEM_DWF_INCLUDE_DIR} + ${MG_OEM_DWF_INCLUDE_DIR}/dwf +) + +# Keep the portable artifacts together so they can be packaged and excluded as one subtree. +set(MG_COMPONENT "portable") +set_install_subdir("portable/") + set(PortableUnmanagedApi_SRCS ${CMAKE_CURRENT_BINARY_DIR}/PortableUnmanagedApi_wrap.cpp ) @@ -6,11 +17,18 @@ add_library(PortableUnmanagedApi SHARED ${PortableUnmanagedApi_SRCS}) add_dependencies(PortableUnmanagedApi IMake) +# PortableApiGen.xml %imports the generated Foundation/Geometry/PlatformBase interfaces. +add_dependencies(PortableUnmanagedApi + FoundationUnmanagedApi + GeometryUnmanagedApi + PlatformBaseUnmanagedApi) +add_definitions(-DDWFCORE_STATIC -DDWFTK_STATIC -DDWFTK_USE_DWFCORE_ZLIB -DDWFTK_BUILD_EXPAT -DWHIP_STATIC_LIB) if (UNIX) add_definitions(-DDOTNETCORE) # NOTE: We don't care where this source is generated because the C# source is only built on windows set(MG_DOTNET_PROJECT_DIR ${CMAKE_CURRENT_BINARY_DIR}) - set_target_properties(PortableUnmanagedApi PROPERTIES INSTALL_RPATH "$ORIGIN") + set_target_properties(PortableUnmanagedApi PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../lib64") endif (UNIX) if (MSVC) add_definitions(-DSWIG_PUBLIC_API) Modified: branches/4.0/MgDev/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -100,7 +100,10 @@ set(MG_SERVER_COMMON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Server/src/Common") set(MG_SERVER_SERVICES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Server/src/Services") set(MG_SERVER_GWS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Server/src/Gws") +set(MG_PORTABLE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Portable") +option(WITH_PORTABLE "Build MgPortable (the portable MapGuide API)" OFF) + set(MG_AUTO_GENERATED_HEADER_DIR "${CMAKE_CURRENT_BINARY_DIR}/IncludeAutogenerated") set(MG_UNIT_TEST_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/UnitTest/TestData") Modified: branches/4.0/MgDev/Common/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Common/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Common/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -25,6 +25,13 @@ if (NOT MG_COMMON_SUBSET_ONLY) add_subdirectory(Renderers) add_subdirectory(Stylization) +elseif (WITH_PORTABLE) + # MgPortable renders through Renderers/Stylization, so a portable common-subset build needs + # them too. Install them beside MgPortable so the common tarball can exclude them. + set_install_subdir("portable/") + add_subdirectory(Renderers) + add_subdirectory(Stylization) + set_install_subdir("") endif (NOT MG_COMMON_SUBSET_ONLY) if (NOT MG_COMMON_SUBSET_ONLY) Modified: branches/4.0/MgDev/Oem/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Oem/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Oem/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -24,9 +24,11 @@ # at the moment, we are just using the system-provided version of GEOS #set(GEOS_ENABLE_TESTS OFF) #add_subdirectory(geos) -if (NOT MG_COMMON_SUBSET_ONLY) +# MgPortable consumes the DWF Toolkit (DrawingService), so the DWF tree is required whenever +# MgPortable is built, including in a common-libs subset tree. +if (NOT MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) add_subdirectory(DWFTK) -endif (NOT MG_COMMON_SUBSET_ONLY) +endif (NOT MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) add_subdirectory(CsMapLibrary) Modified: branches/4.0/MgDev/Portable/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Portable/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -11,5 +11,11 @@ set(MG_COMPONENT "portable") set_install_subdir("portable/") +# MgPortable links the GWS libraries. A full build has already added them under Server; a +# common-libs subset tree has not, so add only them here. +if (MG_COMMON_SUBSET_ONLY) + add_subdirectory(${MG_SERVER_GWS_DIR} ${CMAKE_CURRENT_BINARY_DIR}/Gws) +endif (MG_COMMON_SUBSET_ONLY) + add_subdirectory(MgPortable) add_subdirectory(UnitTest) \ No newline at end of file Modified: branches/4.0/MgDev/Portable/DotNetUnitTest/Program.cs =================================================================== --- branches/4.0/MgDev/Portable/DotNetUnitTest/Program.cs 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/DotNetUnitTest/Program.cs 2026-11-03 13:53:48 UTC (rev 10262) @@ -22,11 +22,10 @@ } catch (AssertException ex) { - //Console.WriteLine(ex.Message); failed++; Console.Write("F"); + Console.WriteLine("\nFAILED: " + method.Method.Name + ": " + ex.Message); Trace.TraceInformation("FAILED: " + method.Method.Name); - //Trace.TraceInformation("F"); } } @@ -34,6 +33,8 @@ { MgPortablePlatform.Initialize("Platform.ini"); + try + { var resTest = new ResourceServiceTests(); try { @@ -140,6 +141,12 @@ Console.WriteLine("\n{0} tests passed {1} tests failed", passed, failed); Trace.TraceInformation("{0} tests passed {1} tests failed", passed, failed); + Environment.ExitCode = failed == 0 ? 0 : 1; + } + finally + { + MgPortablePlatform.Terminate(); + } } } } Modified: branches/4.0/MgDev/Portable/DotNetUnitTest/ResourceServiceTests.cs =================================================================== --- branches/4.0/MgDev/Portable/DotNetUnitTest/ResourceServiceTests.cs 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/DotNetUnitTest/ResourceServiceTests.cs 2026-11-03 13:53:48 UTC (rev 10262) @@ -20,12 +20,13 @@ string resourceDataName = "World_Countries.sdf"; string resourceDataName2 = "New_World_Countries.sdf"; - string repositoryHeaderFileName = "..\\UnitTestFiles\\SampleRepositoryHeader.xm"; - string repositoryContentFileName = "..\\UnitTestFiles\\SampleRepositoryContent.xm"; - string resourceContentFileName = "..\\UnitTestFiles\\TEST.FeatureSource"; - string resourceContentFileName2 = "..\\UnitTestFiles\\TEST.LayerDefinition"; - string dataFileName = "..\\UnitTestFiles\\World_Countries.sdf"; - string packageName = "..\\UnitTestFiles\\Shuttle.zip"; + // Forward slashes are accepted by the .NET file APIs on Windows and are required on POSIX. + string repositoryHeaderFileName = "../UnitTestFiles/SampleRepositoryHeader.xm"; + string repositoryContentFileName = "../UnitTestFiles/SampleRepositoryContent.xm"; + string resourceContentFileName = "../UnitTestFiles/TEST.FeatureSource"; + string resourceContentFileName2 = "../UnitTestFiles/TEST.LayerDefinition"; + string dataFileName = "../UnitTestFiles/World_Countries.sdf"; + string packageName = "../UnitTestFiles/Shuttle.zip"; public void TestStart() @@ -227,7 +228,8 @@ string xml = byteReader.ToString(); Assert.True(xml.Length > 0); Assert.True(xml.Contains("")); + // The fixture ends with a newline, which some platforms preserve. + Assert.True(xml.TrimEnd().EndsWith("")); } catch (MgException ex) { @@ -482,11 +484,11 @@ var resource = new MgResourceIdentifier("Library://UnitTests/Data/TEST.FeatureSource"); - var source = new MgByteSource("..\\UnitTestFiles\\TEST.FeatureSource"); + var source = new MgByteSource("../UnitTestFiles/TEST.FeatureSource"); var reader = source.GetReader(); service.SetResource(resource, reader, null); - var dataSource = new MgByteSource("..\\UnitTestFiles\\TEST.sdf"); + var dataSource = new MgByteSource("../UnitTestFiles/TEST.sdf"); var dataReader = dataSource.GetReader(); service.SetResourceData(resource, "TEST.sdf", "File", dataReader); @@ -512,7 +514,10 @@ { var fact = new MgPortableServiceFactory(); var service = (MgResourceService)fact.CreateService(MgServiceType.ResourceService); - Assert.ThrowsWithCode(() => service.EnumerateUnmanagedData("", true, "", ""), MgExceptionCodes.MgNotImplementedException); + // The type must be Folders, Files or Both; MgResourceUnmanagedDataType is a + // PlatformBase constant class that the SWIG surface does not expose. + Assert.AreEqual(MgMimeType.Xml, service.EnumerateUnmanagedData("", false, "Folders", "").MimeType); + Assert.AreEqual(MgMimeType.Xml, service.EnumerateUnmanagedData("", true, "Files", "").MimeType); } catch (MgException ex) { Modified: branches/4.0/MgDev/Portable/MgPortable/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/MgPortable/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -30,6 +30,10 @@ add_definitions(-DDWFCORE_STATIC -DDWFTK_STATIC -DDWFTK_USE_DWFCORE_ZLIB -DDWFTK_BUILD_EXPAT -DWHIP_STATIC_LIB) add_library(MgPortable${MG_VERSION_SUFFIX} SHARED ${MgPortable_SRCS}) +# The library installs beside the GWS libraries under /portable/lib64, while the +# common libraries it links stay in /lib64. +set_target_properties(MgPortable${MG_VERSION_SUFFIX} PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../lib64") install(TARGETS MgPortable${MG_VERSION_SUFFIX} DESTINATION ${LIB_INSTALL_DIR} COMPONENT ${MG_COMPONENT}) install_symlink(libMgPortable${MG_VERSION_SUFFIX}.so libMgPortable.so ${MG_COMPONENT}) @@ -38,12 +42,15 @@ ${XERCESC_LIBRARIES} ${FDO_LIBRARY} ${FDO_EXPRESSIONENGINE_LIBRARY} - #dwfcore - #dwftk - #dwfwhiptk + # DrawingService.cpp and DrawingServiceUtil.cpp use the DWF Toolkit directly. The static + # libraries are linked the same way MgServerDrawingService links them. + -Wl,--exclude-libs,ALL + dwfcore-1.7.0 + dwftk-7.7.0 MgMdfModel${MG_VERSION_SUFFIX} MgMdfParser${MG_VERSION_SUFFIX} MgFoundation${MG_VERSION_SUFFIX} + MgGeometry${MG_VERSION_SUFFIX} MgPlatformBase${MG_VERSION_SUFFIX} MgSecurity${MG_VERSION_SUFFIX} MgRenderers${MG_VERSION_SUFFIX} @@ -50,5 +57,6 @@ MgStylization${MG_VERSION_SUFFIX} MgGwsCommon${MG_VERSION_SUFFIX} MgGwsQueryEngine${MG_VERSION_SUFFIX} + dl pthread ) \ No newline at end of file Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Rendering/MappingUtil.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Rendering/MappingUtil.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Rendering/MappingUtil.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -35,6 +35,7 @@ #include "SymbolVisitor.h" #include "SymbolDefinition.h" #include "Services/Feature/TransformCache.h" +#include "System/TimerUtil.h" #include @@ -46,7 +47,12 @@ #ifndef _WIN32 #define _wcsnicmp wcsncasecmp -extern long GetTickCount(); +// The timing calls below are Debug-only and use GetTickCount(), which only exists on Windows. +// Provide the same millisecond counter on POSIX so a Debug build links. +static long GetTickCount() +{ + return static_cast(MgPortableTimerUtil::GetTime()); +} #endif /////////////////////////////////////////////////////////////////////////////// Modified: branches/4.0/MgDev/Portable/UnitTest/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -5,7 +5,6 @@ ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../MgPortable ${CMAKE_CURRENT_SOURCE_DIR}/../MgPortable/Services - ${CPPUNIT_INCLUDE_DIR} ${MG_COMMON_DIR}/PlatformBase ${MG_COMMON_DIR}/CoordinateSystem ${MG_COMMON_DIR}/Security @@ -40,10 +39,10 @@ ${ACE_LIBRARY} ${FDO_LIBRARY} ${XERCESC_LIBRARIES} - ${CPPUNIT_LIBRARIES} MgMdfParser${MG_VERSION_SUFFIX} MgMdfModel${MG_VERSION_SUFFIX} MgFoundation${MG_VERSION_SUFFIX} + MgGeometry${MG_VERSION_SUFFIX} MgPlatformBase${MG_VERSION_SUFFIX} MgSecurity${MG_VERSION_SUFFIX} MgPortable${MG_VERSION_SUFFIX} Modified: branches/4.0/MgDev/Portable/UnitTest/TestFeatureService.cpp =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/TestFeatureService.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/TestFeatureService.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -90,7 +90,7 @@ const STRING provider = L"OSGeo.SDF"; #ifdef _WIN32 - const STRING connectionString = L"File=..\\UnitTestFiles\\Sheboygan_Parcels.sdf"; + const STRING connectionString = L"File=../UnitTestFiles/Sheboygan_Parcels.sdf"; #else const STRING connectionString = L"File=../UnitTestFiles/Sheboygan_Parcels.sdf"; #endif Modified: branches/4.0/MgDev/Portable/UnitTest/TestPerformance.cpp =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/TestPerformance.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/TestPerformance.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -81,8 +81,8 @@ MgResourceIdentifier resourceIdentifier1(L"Library://UnitTests/Data/Sheboygan_Parcels.FeatureSource"); #ifdef _WIN32 - STRING resourceContentFileName1 = L"..\\UnitTestFiles\\Sheboygan_Parcels.FeatureSource"; - STRING dataFileName1 = L"..\\UnitTestFiles\\Sheboygan_Parcels.sdf"; + STRING resourceContentFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.FeatureSource"; + STRING dataFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.sdf"; #else STRING resourceContentFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.FeatureSource"; STRING dataFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.sdf"; Modified: branches/4.0/MgDev/Portable/UnitTest/TestResourceService.cpp =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/TestResourceService.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/TestResourceService.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -51,12 +51,12 @@ STRING resourceDataName2 = L"New_World_Countries.sdf"; #ifdef _WIN32 -STRING repositoryHeaderFileName = L"..\\UnitTestFiles\\SampleRepositoryHeader.xml"; -STRING repositoryContentFileName = L"..\\UnitTestFiles\\SampleRepositoryContent.xml"; -STRING resourceContentFileName = L"..\\UnitTestFiles\\TEST.FeatureSource"; -STRING resourceContentFileName2 = L"..\\UnitTestFiles\\TEST.LayerDefinition"; -STRING dataFileName = L"..\\UnitTestFiles\\World_Countries.sdf"; -STRING packageName = L"..\\UnitTestFiles\\Shuttle.zip"; +STRING repositoryHeaderFileName = L"../UnitTestFiles/SampleRepositoryHeader.xml"; +STRING repositoryContentFileName = L"../UnitTestFiles/SampleRepositoryContent.xml"; +STRING resourceContentFileName = L"../UnitTestFiles/TEST.FeatureSource"; +STRING resourceContentFileName2 = L"../UnitTestFiles/TEST.LayerDefinition"; +STRING dataFileName = L"../UnitTestFiles/World_Countries.sdf"; +STRING packageName = L"../UnitTestFiles/Shuttle.zip"; #else STRING repositoryHeaderFileName = L"../UnitTestFiles/SampleRepositoryHeader.xml"; STRING repositoryContentFileName = L"../UnitTestFiles/SampleRepositoryContent.xml"; @@ -958,8 +958,8 @@ resource = new MgResourceIdentifier(L"Library://UnitTests/Data/TEST.FeatureSource"); #ifdef _WIN32 - STRING rcName = L"..\\UnitTestFiles\\TEST.FeatureSource"; - STRING dfName = L"..\\UnitTestFiles\\TEST.sdf"; + STRING rcName = L"../UnitTestFiles/TEST.FeatureSource"; + STRING dfName = L"../UnitTestFiles/TEST.sdf"; #else STRING rcName = L"../UnitTestFiles/TEST.FeatureSource"; STRING dfName = L"../UnitTestFiles/TEST.sdf"; @@ -1148,7 +1148,7 @@ pService->DeleteResource(&resId); } - Ptr byteSource = new MgByteSource(L"..\\UnitTestFiles\\PackageTest.mgp"); + Ptr byteSource = new MgByteSource(L"../UnitTestFiles/PackageTest.mgp"); Ptr byteReader = byteSource->GetReader(); //This package contains an UPDATEREPOSITORY directive, which should no-op when loaded by mg-desktop Modified: branches/4.0/MgDev/Portable/UnitTest/TestServiceFactory.cpp =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/TestServiceFactory.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/TestServiceFactory.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -897,23 +897,23 @@ MgResourceIdentifier resourceIdentifier8(L"Library://UnitTests/Data/FdoJoin.FeatureSource"); MgResourceIdentifier resourceIdentifier9(L"Library://UnitTests/Data/SecuredCredentials.FeatureSource"); #ifdef _WIN32 - STRING resourceContentFileName1 = L"..\\UnitTestFiles\\Sheboygan_Parcels.FeatureSource"; - STRING resourceContentFileName2 = L"..\\UnitTestFiles\\Redding_Parcels.FeatureSource"; - STRING resourceContentFileName3 = L"..\\UnitTestFiles\\Sheboygan_BuildingOutlines.FeatureSource"; - STRING resourceContentFileName4 = L"..\\UnitTestFiles\\Sheboygan_VotingDistricts.FeatureSource"; - STRING resourceContentFileName5 = L"..\\UnitTestFiles\\TESTChainedInner1ToManyJoin.FeatureSource"; - STRING resourceContentFileName6 = L"..\\UnitTestFiles\\Empty.FeatureSource"; - STRING resourceContentFileName7 = L"..\\UnitTestFiles\\Sheboygan_Parcels_Writable.FeatureSource"; - STRING resourceContentFileName8 = L"..\\UnitTestFiles\\UT_FdoJoin.FeatureSource"; - STRING resourceContentFileName9 = L"..\\UnitTestFiles\\SecuredCredentials.fs"; - STRING dataFileName1 = L"..\\UnitTestFiles\\Sheboygan_Parcels.sdf"; - STRING dataFileName2 = L"..\\UnitTestFiles\\Redding_Parcels.shp"; - STRING dataFileName3 = L"..\\UnitTestFiles\\Redding_Parcels.dbf"; - STRING dataFileName4 = L"..\\UnitTestFiles\\Redding_Parcels.shx"; - STRING dataFileName5 = L"..\\UnitTestFiles\\Sheboygan_BuildingOutlines.sdf"; - STRING dataFileName6 = L"..\\UnitTestFiles\\Sheboygan_VotingDistricts.sdf"; - STRING dataFileName7 = L"..\\UnitTestFiles\\Empty.sdf"; - STRING dataFileName8 = L"..\\UnitTestFiles\\JoinTest.sqlite"; + STRING resourceContentFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.FeatureSource"; + STRING resourceContentFileName2 = L"../UnitTestFiles/Redding_Parcels.FeatureSource"; + STRING resourceContentFileName3 = L"../UnitTestFiles/Sheboygan_BuildingOutlines.FeatureSource"; + STRING resourceContentFileName4 = L"../UnitTestFiles/Sheboygan_VotingDistricts.FeatureSource"; + STRING resourceContentFileName5 = L"../UnitTestFiles/TESTChainedInner1ToManyJoin.FeatureSource"; + STRING resourceContentFileName6 = L"../UnitTestFiles/Empty.FeatureSource"; + STRING resourceContentFileName7 = L"../UnitTestFiles/Sheboygan_Parcels_Writable.FeatureSource"; + STRING resourceContentFileName8 = L"../UnitTestFiles/UT_FdoJoin.FeatureSource"; + STRING resourceContentFileName9 = L"../UnitTestFiles/SecuredCredentials.fs"; + STRING dataFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.sdf"; + STRING dataFileName2 = L"../UnitTestFiles/Redding_Parcels.shp"; + STRING dataFileName3 = L"../UnitTestFiles/Redding_Parcels.dbf"; + STRING dataFileName4 = L"../UnitTestFiles/Redding_Parcels.shx"; + STRING dataFileName5 = L"../UnitTestFiles/Sheboygan_BuildingOutlines.sdf"; + STRING dataFileName6 = L"../UnitTestFiles/Sheboygan_VotingDistricts.sdf"; + STRING dataFileName7 = L"../UnitTestFiles/Empty.sdf"; + STRING dataFileName8 = L"../UnitTestFiles/JoinTest.sqlite"; #else STRING resourceContentFileName1 = L"../UnitTestFiles/Sheboygan_Parcels.FeatureSource"; STRING resourceContentFileName2 = L"../UnitTestFiles/Redding_Parcels.FeatureSource"; Modified: branches/4.0/MgDev/Portable/UnitTest/main.cpp =================================================================== --- branches/4.0/MgDev/Portable/UnitTest/main.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Portable/UnitTest/main.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -53,6 +53,15 @@ #if !defined(USING_VLD) _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); + // The leak report is written to the debugger's output, which a plain command-line run + // discards. Set MG_CRT_LEAK_CHECK=1 to send it to stderr instead, so it shows up in the + // run's console output and in the log file a test run tees to. + if (getenv("MG_CRT_LEAK_CHECK") != NULL) + { + _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); + } + #ifdef _DEBUG // to debug memory leaks, set a breakpoint here and set iBlock // to the block allocation you want to break on Modified: branches/4.0/MgDev/Server/src/Gws/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Server/src/Gws/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Server/src/Gws/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -1,7 +1,14 @@ set(MG_COMPONENT "common") set(CMAKE_INSTALL_PREFIX "${MG_INSTALL_PREFIX}") -set_install_subdir("") +# In a common-libs subset tree the GWS libraries are only built for MgPortable. Install them +# beside it so the common tarball can exclude them wholesale. +if (MG_COMMON_SUBSET_ONLY AND WITH_PORTABLE) + set(MG_COMPONENT "portable") + set_install_subdir("portable/") +else (MG_COMMON_SUBSET_ONLY AND WITH_PORTABLE) + set_install_subdir("") +endif (MG_COMMON_SUBSET_ONLY AND WITH_PORTABLE) add_subdirectory(GwsCommon) add_subdirectory(GwsQueryEngine) Modified: branches/4.0/MgDev/Server/src/Gws/GwsCommon/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsCommon/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Server/src/Gws/GwsCommon/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -24,5 +24,13 @@ ) add_library(MgGwsCommon${MG_VERSION_SUFFIX} SHARED ${GwsCommon_SRCS}) +# A portable build links the GWS libraries into libMgPortable with --no-undefined, so their own +# FDO/Foundation/Geometry dependencies have to be explicit rather than resolved by the server +# process at load time. +target_link_libraries(MgGwsCommon${MG_VERSION_SUFFIX} + ${FDO_LIBRARY} + MgFoundation${MG_VERSION_SUFFIX} + MgGeometry${MG_VERSION_SUFFIX} +) install(TARGETS MgGwsCommon${MG_VERSION_SUFFIX} DESTINATION ${LIB_INSTALL_DIR} COMPONENT ${MG_COMPONENT}) install_symlink(libMgGwsCommon${MG_VERSION_SUFFIX}.so libMgGwsCommon.so ${MG_COMPONENT}) \ No newline at end of file Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/CMakeLists.txt =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/CMakeLists.txt 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/CMakeLists.txt 2026-11-03 13:53:48 UTC (rev 10262) @@ -50,5 +50,10 @@ ) add_library(MgGwsQueryEngine${MG_VERSION_SUFFIX} SHARED ${GwsQueryEngine_SRCS}) +target_link_libraries(MgGwsQueryEngine${MG_VERSION_SUFFIX} + ${FDO_LIBRARY} + ${FDO_EXPRESSIONENGINE_LIBRARY} + MgGwsCommon${MG_VERSION_SUFFIX} +) install(TARGETS MgGwsQueryEngine${MG_VERSION_SUFFIX} DESTINATION ${LIB_INSTALL_DIR} COMPONENT ${MG_COMPONENT}) install_symlink(libMgGwsQueryEngine${MG_VERSION_SUFFIX}.so libMgGwsQueryEngine.so ${MG_COMPONENT}) \ No newline at end of file Modified: branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFlatFdoReader.cpp =================================================================== --- branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFlatFdoReader.cpp 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/Server/src/Gws/GwsQueryEngine/GwsFlatFdoReader.cpp 2026-11-03 13:53:48 UTC (rev 10262) @@ -26,7 +26,16 @@ #include "GwsQueryEngineImp.h" #ifndef _WIN32 -extern long GetTickCount(); +#include + +// CGwsTimer is compiled on every platform but the Windows GetTickCount() it calls does not exist +// on POSIX. Only GWSTIMER expands to it, which is a Debug-only macro. +static long GetTickCount() +{ + timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000) + (tv.tv_usec / 1000); +} #endif Added: branches/4.0/MgDev/cmake/configs/Platform.ini.in =================================================================== --- branches/4.0/MgDev/cmake/configs/Platform.ini.in (rev 0) +++ branches/4.0/MgDev/cmake/configs/Platform.ini.in 2026-11-03 13:53:48 UTC (rev 10262) @@ -0,0 +1,336 @@ +# ***************************************************************************** +# MapGuide Portable Platform Configuration File +# +# The following configuration is based on a single CPU with a single core. +# +# WARNING: BE VERY CAREFUL WHEN MODIFYING THIS FILE AS IT COULD +# ADVERSLY IMPACT APPLICATION PERFORMANCE +# +# When saving this file use a UTF-8 encoding. +# +# ***************************************************************************** +# COMMON VALIDATION INFORMATION +# +# (Unless otherwise noted under the property description) +# ----------------------------------------------------------------------------- +# String Property Range of String Length Reserved Characters +# ----------------------------------------------------------------------------- +# File Name 0 < Length <= 128 \/:*?"<>| +# Folder Name 0 < Length <= 128 \/:*?"<>| +# Log Parameters 0 <= Length <= 1024 +# Password 0 <= Length <= 64 \t\r\n\v\f +# Path 0 < Length <= 255 *?"<>| +# ----------------------------------------------------------------------------- +# Numeric Property Range of Numeric Value +# ----------------------------------------------------------------------------- +# Port Number 0 <= Value <= 65535 +# ***************************************************************************** + +[GeneralProperties] +# ***************************************************************************** +# G E N E R A L +# +# Property Name Description +# ----------------------------------------------------------------------------- +# DefaultMessageLocale ISO 639-1 name for the message locale +# Length = 2 +# Example: en +# FdoPath Path where FDO is installed +# Locale The server's locale. If left blank it will default to the operating system locale +# 0 <= Length <= 255 +# Format : lang[_country_region.code_page] +# Example (Windows): English_United States.1252 +# Example Linux) : en_US.iso88591 +# LogsDelimiter Delimiter that separates the data fields in +# the log files +# LogsDetail Level of logging detail per service +# 0 - Errors without parameters (default) +# 1 - Warnings and Errors with parameters +# 2 - Trace, Warnings, and Errors +# 3 - Internal Trace, Trace, Warnings, and Errors +# Service keys: ResourceService, DrawingService, FeatureService, MappingService, +# RenderingService, TileService, KmlService, ServerAdminService, SiteService +# Example: MappingService:3,FeatureService:1 +# LogsPath Path where log files are stored +# MentorDictionaryPath Path where the CS-Map cooridnate system dictionaries is installed +# PreCacheMaps The list of maps to precache at server startup. +# Value = Map resource name(s) separated by "," +# Example: Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition +# Renderer Image renderer to use (GD or AGG) +# ResourcesPath Path where the localization resource files are stored +# TempPath Path where the temporary files are stored +# WfsDocumentPath Document path location for WFS documents +# WmsDocumentPath Document path location for WMS documents +# ***************************************************************************** +DefaultMessageLocale = en +FdoPath = @FDO_LIBRARY_DIR@ +MentorDictionaryPath = @MG_INSTALL_COORDSYS_PREFIX_ABS@ +Locale = +LogsDelimiter = \t +LogsDetail = +LogsPath = Logs/ +PreCacheMaps = +Renderer = AGG +ResourcesPath = Resources/ +TempPath = Temp/ +WfsDocumentPath = Wfs/ +WmsDocumentPath = Wms/ + +[DrawingServiceProperties] +# ***************************************************************************** +# D R A W I N G S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# ***************************************************************************** + +[FeatureServiceProperties] +# ***************************************************************************** +# F E A T U R E S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# CacheSize Max # of internal data objects to cache +# (schemas, classes, etc...) +# 0 < Value <= 5000 +# CacheTimeLimit Time duration in seconds for how long to +# cache the internal data objects +# 0 < Value <= 2147483647 +# CacheTimerInterval Time interval in seconds for when the server +# checks for expired cache entries +# 0 < Value <= 2147483647 +# DataConnectionPoolEnabled FDO connection pooling +# 0 = disabled, 1 = enabled +# DataConnectionPoolExcludedProviders The list of providers to exclude from connection pooling. +# 0 <= Length <= 1024 +# Value = provider name(s) separated by "," +# Example: OSGeo.SDF,OSGeo.SHP +# JoinQueryBatchSize Join query batch size +# 1 < Value <= 10000 +# UseFdoJoinOptimization Determines whether to use FDO Join APIs for feature joins between +# classes from the same Feature Source for FDO providers that support it +# 1 = Use FDO Join APIs if possible, 0 = Fallback to GwsQueryEngine +# ***************************************************************************** +CacheSize = 100 +CacheTimeLimit = 86400 +CacheTimerInterval = 3600 +DataConnectionPoolEnabled = 1 +DataConnectionPoolExcludedProviders= OSGeo.SDF,OSGeo.SHP,OSGeo.SQLite +JoinQueryBatchSize = 1000 +UseFdoJoinOptimization = 1 + +[MappingServiceProperties] +# ***************************************************************************** +# M A P P I N G S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# LegendFont Font to use when rendering legend elements +# 0 < Length <= 255 +# ***************************************************************************** +LegendFont = Arial + +[RenderingServiceProperties] +# ***************************************************************************** +# R E N D E R I N G S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# TileExtentOffset Max request extent offset to use when +# requesting features for a tile, specified as +# a factor of the tile size +# 0.0 <= Value <= 1.0 +# RasterGridSize Size of raster re-projection grid in pixels +# 0 < Value <= 2147483647 +# MinRasterGridSize Minimum size of raster re-projection grid in +# pixels. This should be less than RasterGridSize. +# 0 < Value <= 2147483647 +# RasterGridSizeOverrideRatio If the RasterGridSize is larger than the +# image's height or width multiplied by the +# RasterGridSizeOverrideRatio, then the +# RasterGridSize is overridden with this value. +# Set to 0 or 1 to disable the override. +# 0.0 <= Value <= 1.0 +# RasterGridSizeForPlot Size of raster re-projection grid in pixels +# for plot +# MinRasterGridSizeForPlot Minimum size of raster re-projection grid in +# pixels for plot. This must be less than +# RasterGridSizeForPlot. +# RasterGridSizeOverrideRatioForPlot If the RasterGridSizeForPlot is larger than the +# image's height or width multiplied by the +# RasterGridSizeOverrideRatioForPlot, then the +# RasterGridSizeForPlot is overridden with this +# value. +# Change this setting to 1 to disable the +# override. +# RenderSelectionBatchSize The batch size to use when rendering a selection +# ClampPoints Specifies whether point coordinates are clamped +# to integer values before passing them to the +# AGG renderer (experimental) +# 0 = false and 1 = true +# GeneralizeData Specifies whether feature geometry is generalized +# before being rendered (experimental) +# 0 = false and 1 = true +# ***************************************************************************** +TileExtentOffset = 0.35 +RasterGridSize = 100 +MinRasterGridSize = 10 +RasterGridSizeOverrideRatio = 0.25 +RasterGridSizeForPlot = 50 +MinRasterGridSizeForPlot = 5 +RasterGridSizeOverrideRatioForPlot = 0.10 +RenderSelectionBatchSize = 50000 +ClampPoints = 0 +GeneralizeData = 0 + +[ResourceServiceProperties] +# ***************************************************************************** +# R E S O U R C E S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# LibraryRepositoryPath Path where the Library repository is stored +# LibraryResourceDataFilePath Path where the Library resource data files +# are stored +# PackagesPath Path where the resource packages can be found +# ResourceDataFileTrashFolderName Folder where the repository trash files are +# stored +# ResourceSchemaFilePath Path where the resource schema files are +# stored +# SessionRepositoryPath Path where the Session repository is stored +# SessionResourceDataFilePath Path where the Session resource data files +# are stored +# SiteRepositoryPath Path where the Site repository is stored +# ***************************************************************************** +LibraryRepositoryPath = Repositories/Library/Content/ +LibraryResourceDataFilePath = Repositories/Library/DataFiles/ +PackagesPath = Packages/ +ResourceDataFileTrashFolderName = Trash +ResourceSchemaFilePath = Schema/ +SessionRepositoryPath = Repositories/Session/Content/ +SessionResourceDataFilePath = Repositories/Session/DataFiles/ +SiteRepositoryPath = Repositories/Site/ + +[TileServiceProperties] +# ***************************************************************************** +# T I L E S E R V I C E +# +# Property Name Description +# ----------------------------------------------------------------------------- +# RenderOnly Renders the tile only +# 0 = false and 1 = true +# TileCachePath Root path of the image tile cache +# TileColumnsPerFolder Number of columns of tiles per folder +# 0 < Value <= 1000 +# TileRowsPerFolder Number of rows of tiles per folder +# 0 < Value <= 1000 +# DefaultTileSizeX Width of generated tiles in pixels +# 50 < value <= 10000 +# DefaultTileSizeY Height of generated tiles in pixels +# 50 < value <= 10000 +# ImageFormat Image format for generated tiles +# PNG, PNG8, GIF or JPG +# ***************************************************************************** +RenderOnly = 0 +TileCachePath = Repositories/TileCache/ +TileColumnsPerFolder = 30 +TileRowsPerFolder = 30 +DefaultTileSizeX = 300 +DefaultTileSizeY = 300 +ImageFormat = PNG + +[AccessLogProperties] +# ***************************************************************************** +# A C C E S S L O G +# +# Property Name Description +# ----------------------------------------------------------------------------- +# Enabled 0 = log disabled, 1 = log enabled +# Filename Name of the log file +# Parameters Log parameters +# ***************************************************************************** +Enabled = 1 +Filename = "Access.log" +Parameters = CLIENT,CLIENTIP,USER,OPID + +[AdminLogProperties] +# ***************************************************************************** +# A D M I N L O G (Not used by mg-portable) +# +# Property Name Description +# ----------------------------------------------------------------------------- +# Enabled 0 = log disabled, 1 = log enabled +# Filename Name of the log file +# Parameters Log parameters +# ***************************************************************************** +Enabled = 1 +Filename = "Admin.log" +Parameters = CLIENT,CLIENTIP,USER,OPID + +[AuthenticationLogProperties] +# ***************************************************************************** +# A U T H E N T I C A T I O N L O G (Not used by mg-portable) +# +# Property Name Description +# ----------------------------------------------------------------------------- +# Enabled 0 = log disabled, 1 = log enabled +# Filename Name of the log file +# Parameters Log parameters +# ***************************************************************************** +Enabled = 1 +Filename = "Authentication.log" +Parameters = CLIENT,CLIENTIP,USER + +[ErrorLogProperties] +# ***************************************************************************** +# E R R O R L O G +# +# Property Name Description +# ----------------------------------------------------------------------------- +# Enabled 0 = log disabled, 1 = log enabled +# Filename Name of the log file +# Parameters Log parameters +# ***************************************************************************** +Enabled = 1 +Filename = "Error.log" +Parameters = CLIENT,CLIENTIP,USER,ERROR,STACKTRACE + +[TraceLogProperties] +# ***************************************************************************** +# T R A C E L O G +# +# Property Name Description +# ----------------------------------------------------------------------------- +# Enabled 0 = log disabled, 1 = log enabled +# Filename Name of the log file +# Parameters Log parameters +# ***************************************************************************** +Enabled = 0 +Filename = "Trace.log" +Parameters = INFO + +[FontAliases] +# ***************************************************************************** +# F O N T A L I A S E S +# This section is used to map a font family name to another. The left side +# is a name that might be specified by the user and the right side is what +# font to actually use. +# ***************************************************************************** +?? ???? = MS Gothic +?? ????? = MS PGothic +?? ?? = MS Mincho +?? ??? = MS PMincho +?? = Gulim +??? = GulimChe +?? = Dotum +??? = DotumChe +?? = Batang +??? = BatangChe +?? = Gungsuh +??? = GungsuhChe + +[UnmanagedDataMappings] +# ***************************************************************************** +# U N M A N A G E D D A T A M A P P I N G S +# This section is used to map a mapping name to an unmanaged data folder. +# ***************************************************************************** \ No newline at end of file Modified: branches/4.0/MgDev/cmake/modules/FindMapGuideThirdparty.cmake =================================================================== --- branches/4.0/MgDev/cmake/modules/FindMapGuideThirdparty.cmake 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/cmake/modules/FindMapGuideThirdparty.cmake 2026-11-03 13:53:48 UTC (rev 10262) @@ -77,7 +77,7 @@ find_package(XercesC REQUIRED) endif (INTERNAL_XERCES) -if (NOT MG_COMMON_SUBSET_ONLY) +if (NOT MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) if (INTERNAL_GD) message(STATUS "Using internal GD") @@ -155,7 +155,7 @@ find_package(PNG REQUIRED) endif (INTERNAL_PNG) -endif (NOT MG_COMMON_SUBSET_ONLY) +endif (NOT MG_COMMON_SUBSET_ONLY OR WITH_PORTABLE) if (INTERNAL_JSON) message(STATUS "Using internal jsoncpp") @@ -199,4 +199,10 @@ endif (NOT MG_COMMON_SUBSET_ONLY) +# MgPortable and the GWS libraries a portable common-subset build adds need FDO. The rest of the +# subset does not, so only look for it when portable is on. +if (MG_COMMON_SUBSET_ONLY AND WITH_PORTABLE) + find_package(FDO REQUIRED) +endif (MG_COMMON_SUBSET_ONLY AND WITH_PORTABLE) + find_package(TCLAP REQUIRED) \ No newline at end of file Modified: branches/4.0/MgDev/cmake_bootstrap.sh =================================================================== --- branches/4.0/MgDev/cmake_bootstrap.sh 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/cmake_bootstrap.sh 2026-11-03 13:53:48 UTC (rev 10262) @@ -10,6 +10,7 @@ INTERNAL_JSON=0 INTERNAL_GEOS=0 COMMON_SUBSET_ONLY=0 +WITH_PORTABLE=0 BUILD_CPU=32 BUILD_CONFIG=Release HAVE_SYSTEM_XERCES=0 @@ -31,6 +32,11 @@ --common-subset-only) COMMON_SUBSET_ONLY=1 ;; + --with-portable) + # MgPortable needs the renderers/stylization libraries even in a common-subset + # tree, so the graphics thirdparty libs have to be built there too. + WITH_PORTABLE=1 + ;; --with-all-internal) INTERNAL_ACE=1 INTERNAL_ZLIB=1 @@ -91,6 +97,8 @@ echo " --with-internal-json [Build/Use internal JsonCpp]" echo " --with-internal-geos [Build/Use internal GEOS]" echo " --with-all-internal [Build/Use all internal libs]" + echo " --common-subset-only [Only prepare the common libs subset]" + echo " --with-portable [Also build the graphics libs a portable subset needs]" echo " --with-ccache [Use ccache]" exit ;; @@ -197,7 +205,13 @@ check_build fi mkdir -p "${OEM_WORK_DIR}/gd" -if [ "$COMMON_SUBSET_ONLY" != "1" ] && [ "$INTERNAL_ZLIB" = "1" ]; then +# Renderers/Stylization are part of a portable common-subset build, and they need the graphics +# thirdparty libraries, so build them even when the rest of the subset does not use them. +BUILD_GRAPHICS_LIBS=0 +if [ "$COMMON_SUBSET_ONLY" != "1" ] || [ "$WITH_PORTABLE" = "1" ]; then + BUILD_GRAPHICS_LIBS=1 +fi +if [ "$BUILD_GRAPHICS_LIBS" = "1" ] && [ "$INTERNAL_ZLIB" = "1" ]; then echo "Building zlib (internal)" cp -Rf "${SOURCE_DIR}/Oem/gd/zlib" "${OEM_WORK_DIR}/gd/zlib" cd "${OEM_WORK_DIR}/gd/zlib" || exit @@ -211,7 +225,7 @@ make && make install check_build fi -if [ "$COMMON_SUBSET_ONLY" != "1" ] && [ "$INTERNAL_LIBPNG" = "1" ]; then +if [ "$BUILD_GRAPHICS_LIBS" = "1" ] && [ "$INTERNAL_LIBPNG" = "1" ]; then echo "Building libpng (internal)" cp -Rf "${SOURCE_DIR}/Oem/gd/lpng" "${OEM_WORK_DIR}/gd/lpng" cd "${OEM_WORK_DIR}/gd/lpng" || exit @@ -230,7 +244,7 @@ check_build fi fi -if [ "$COMMON_SUBSET_ONLY" != "1" ] && [ "$INTERNAL_JPEG" = "1" ]; then +if [ "$BUILD_GRAPHICS_LIBS" = "1" ] && [ "$INTERNAL_JPEG" = "1" ]; then echo "Building libjpeg (internal)" cp -Rf "${SOURCE_DIR}/Oem/gd/jpeg" "${OEM_WORK_DIR}/gd/jpeg" cd "${OEM_WORK_DIR}/gd/jpeg" || exit @@ -255,7 +269,7 @@ check_build fi fi -if [ "$COMMON_SUBSET_ONLY" != "1" ] && [ "$INTERNAL_FREETYPE" = "1" ]; then +if [ "$BUILD_GRAPHICS_LIBS" = "1" ] && [ "$INTERNAL_FREETYPE" = "1" ]; then echo "Building freetype (internal)" cp -Rf "${SOURCE_DIR}/Oem/gd/freetype" "${OEM_WORK_DIR}/gd/freetype" cd "${OEM_WORK_DIR}/gd/freetype" || exit @@ -286,7 +300,7 @@ PHP_ZLIB_DIR="$OEM_WORK_DIR/install/zlib" fi -if [ "$COMMON_SUBSET_ONLY" != "1" ] && [ "$INTERNAL_GD" = "1" ]; then +if [ "$BUILD_GRAPHICS_LIBS" = "1" ] && [ "$INTERNAL_GD" = "1" ]; then echo "Building GD (internal)" cp -Rf "${SOURCE_DIR}/Oem/gd/gd" "${OEM_WORK_DIR}/gd/gd" cd "${OEM_WORK_DIR}/gd/gd" || exit Modified: branches/4.0/MgDev/cmake_build.sh =================================================================== --- branches/4.0/MgDev/cmake_build.sh 2026-11-03 12:22:56 UTC (rev 10261) +++ branches/4.0/MgDev/cmake_build.sh 2026-11-03 13:53:48 UTC (rev 10262) @@ -60,6 +60,8 @@ echo " --oem-working-dir [OEM build directory, default: ~/mapguide_oem_build]" echo " --with-asan [build with ASAN (if available)]" echo " --with-gold [link with gold instead of ld (if available)]" + echo " --common-subset-only [Configure the common libs subset]" + echo " --with-portable [Build MgPortable and its SWIG glue]" echo " --ninja [Use ninja build system]" echo " --help [Display usage]" exit From svn_mapguide at osgeo.org Tue Nov 3 06:32:36 2026 From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org) Date: Tue, 3 Nov 2026 06:32:36 -0800 Subject: [mapguide-commits] r10263 - in branches/4.0/MgDev/Portable/MgPortable/Services: . Feature Feature/Commands Message-ID: <20261103143236.48857143931@trac.osgeo.org> Author: jng Date: 2026-11-03 06:32:34 -0800 (Tue, 03 Nov 2026) New Revision: 10263 Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/Commands/UpdateFeatures.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FdoForcedOneToOneFeatureReader.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureConnection.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureUtil.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/GwsConnectionPool.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.cpp branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.h branches/4.0/MgDev/Portable/MgPortable/Services/ScrollableFeatureReader.cpp Log: #2897: Plug ASAN reported leaks in MapGuide Portable Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/Commands/UpdateFeatures.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/Commands/UpdateFeatures.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/Commands/UpdateFeatures.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -59,7 +59,7 @@ bool useTransaction) { Ptr propCol; - FdoITransaction* transaction = NULL; + FdoPtr transaction; bool commited = false; MG_FEATURE_SERVICE_TRY() Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FdoForcedOneToOneFeatureReader.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FdoForcedOneToOneFeatureReader.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FdoForcedOneToOneFeatureReader.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -267,7 +267,7 @@ bool append = false; wchar_t tbuff[256]; //Don't bother with IsNull() checks. These are identity property values, how can they be null? - FdoStringElement* el = m_idPropNames->GetItem(i); + FdoPtr el = m_idPropNames->GetItem(i); FdoStringP str = el->GetString(); FdoString* name = (FdoString*)str; Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureConnection.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureConnection.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureConnection.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -91,6 +91,8 @@ FdoInt32 iRefCount = m_fdoConn->GetRefCount(); ACE_DEBUG((LM_INFO, ACE_TEXT("MgPortableFeatureConnection::Close() - refcount %d\n"), iRefCount)); #endif + // ReturnConnection takes over this object's reference to the connection: it either + // hands it to the pool (which holds its own reference) or closes it and releases it. MgPortableFdoConnectionPool::ReturnConnection(this); m_fdoConn = NULL; } Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureUtil.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureUtil.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/FeatureUtil.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -3476,7 +3476,7 @@ case FdoExpressionItemType_Function: { FdoFunction* func = static_cast(expr); - FdoExpressionCollection* funcArgs = func->GetArguments(); + FdoPtr funcArgs = func->GetArguments(); for (FdoInt32 i = 0; i < funcArgs->GetCount(); i++) { FdoPtr arg = funcArgs->GetItem(i); @@ -3495,7 +3495,7 @@ case FdoExpressionItemType_UnaryExpression: { FdoUnaryExpression* unexpr = static_cast(expr); - FdoExpression* inner = unexpr->GetExpression(); + FdoPtr inner = unexpr->GetExpression(); FdoPtr result = ExtractIdentifiers(inner); for (FdoInt32 i = 0; i < result->GetCount(); i++) Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/GwsConnectionPool.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/GwsConnectionPool.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/GwsConnectionPool.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -40,6 +40,10 @@ MgPortableFeatureConnection * conn = (*iter).second; if(conn) { + // The pool took these connections over from the reader, which owns them + // (OwnReader()), so Close() is what returns the FDO connection to the pool. + // It is a no-op for a connection a reader Close() already returned. + conn->Close(); conn->Release (); } } Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -19,7 +19,7 @@ MgPortableJoinFeatureReader::MgPortableJoinFeatureReader(MgPortableGwsFeatureReader* reader) { - m_reader = SAFE_ADDREF(reader); + m_reader = reader; m_propertyNamesInitialized = false; } Modified: branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.h =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.h 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/Feature/JoinFeatureReader.h 2026-11-03 14:32:34 UTC (rev 10263) @@ -562,7 +562,10 @@ FdoObjectType MgObjectPropertyTypeToFdoObjectType(INT32 type); FdoOrderType MgOrderingOptionToFdoOrderType(INT32 type); - Ptr m_reader; + // Weak reference: the reader owns this object and holds it through + // MgPortableGwsFeatureReader::m_joinReader, so a counted reference here + // would make the pair a cycle that nothing can ever release. + MgPortableGwsFeatureReader* m_reader; // Used for access by index readers FdoStringsP m_propertyNames; Modified: branches/4.0/MgDev/Portable/MgPortable/Services/ScrollableFeatureReader.cpp =================================================================== --- branches/4.0/MgDev/Portable/MgPortable/Services/ScrollableFeatureReader.cpp 2026-11-03 13:53:48 UTC (rev 10262) +++ branches/4.0/MgDev/Portable/MgPortable/Services/ScrollableFeatureReader.cpp 2026-11-03 14:32:34 UTC (rev 10263) @@ -29,8 +29,9 @@ m_scrollReader = FDO_SAFE_ADDREF(reader); m_connection = SAFE_ADDREF(conn); - // The reader takes ownership of the FDO connection - m_connection->OwnReader(); + // The MgPortableFeatureReader constructor has already taken ownership of the FDO + // connection via OwnReader(). Calling OwnReader() again takes a second reference on + // it, which nothing releases, so the connection (and its schema cache) survives. } MgPortableScrollableFeatureReader::~MgPortableScrollableFeatureReader()