@@ -239,10 +239,12 @@ describe("ensureGpkgFeatureCountSync", () => {
239239 ] ) ;
240240 } ) ;
241241
242- it ( "matches table names case-insensitively across metadata tables " , ( ) => {
242+ it ( "matches table names case-insensitively and normalises to the canonical casing " , ( ) => {
243243 // gpkg_contents and gpkg_ogr_contents disagree on casing for the same
244- // (case-insensitive) SQLite table. The repair must treat them as one table
245- // and UPDATE the existing NULL row rather than INSERT a duplicate.
244+ // (case-insensitive) SQLite table. The repair must treat them as one table,
245+ // UPDATE the existing NULL row rather than INSERT a duplicate, and rewrite
246+ // table_name to the gpkg_contents spelling so GDAL's case-sensitive lookup
247+ // finds it.
246248 const db : Database = new SQL . Database ( ) ;
247249 db . run ( `
248250 CREATE TABLE gpkg_contents (
@@ -262,7 +264,34 @@ describe("ensureGpkgFeatureCountSync", () => {
262264 const patched = ensureGpkgFeatureCountSync ( SQL , original ) ;
263265 assert . notEqual ( patched , original ) ;
264266 assert . deepEqual ( readOgrContents ( patched ) , [
265- { table_name : "places" , feature_count : 3 } ,
267+ { table_name : "Places" , feature_count : 3 } ,
268+ ] ) ;
269+ } ) ;
270+
271+ it ( "repairs a non-ASCII table name whose count is NULL" , ( ) => {
272+ // SQLite's lower() is ASCII-only, so a `lower(table_name) = :key` predicate
273+ // would never match a non-ASCII name; matching on the exact stored name
274+ // keeps the UPDATE working here.
275+ const db : Database = new SQL . Database ( ) ;
276+ db . run ( `
277+ CREATE TABLE gpkg_contents (
278+ table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL, srs_id INTEGER
279+ );
280+ CREATE TABLE "Über" (fid INTEGER PRIMARY KEY, geom BLOB);
281+ INSERT INTO gpkg_contents VALUES ('Über', 'features', 4326);
282+ INSERT INTO "Über" (geom) VALUES (NULL), (NULL);
283+ CREATE TABLE gpkg_ogr_contents (
284+ table_name TEXT NOT NULL PRIMARY KEY, feature_count INTEGER
285+ );
286+ INSERT INTO gpkg_ogr_contents (table_name, feature_count) VALUES ('Über', NULL);
287+ ` ) ;
288+ const original = db . export ( ) ;
289+ db . close ( ) ;
290+
291+ const patched = ensureGpkgFeatureCountSync ( SQL , original ) ;
292+ assert . notEqual ( patched , original ) ;
293+ assert . deepEqual ( readOgrContents ( patched ) , [
294+ { table_name : "Über" , feature_count : 2 } ,
266295 ] ) ;
267296 } ) ;
268297
@@ -321,6 +350,31 @@ describe("ensureGpkgFeatureCountSync", () => {
321350 ] ) ;
322351 } ) ;
323352
353+ it ( "skips an unreadable phantom table and still repairs the others" , ( ) => {
354+ // gpkg_contents lists a table that does not exist as a real SQLite table
355+ // (a deleted/virtual/view entry). count(*) on it throws; the repair must
356+ // skip it and still patch the readable feature table.
357+ const db : Database = new SQL . Database ( ) ;
358+ db . run ( `
359+ CREATE TABLE gpkg_contents (
360+ table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL, srs_id INTEGER
361+ );
362+ CREATE TABLE real_table (fid INTEGER PRIMARY KEY, geom BLOB);
363+ INSERT INTO gpkg_contents VALUES ('real_table', 'features', 4326);
364+ INSERT INTO gpkg_contents VALUES ('ghost_table', 'features', 4326);
365+ INSERT INTO real_table (geom) VALUES (NULL), (NULL);
366+ ` ) ;
367+ const original = db . export ( ) ;
368+ db . close ( ) ;
369+
370+ const patched = ensureGpkgFeatureCountSync ( SQL , original ) ;
371+ assert . notEqual ( patched , original ) ;
372+ // ghost_table is silently skipped; real_table is repaired.
373+ assert . deepEqual ( readOgrContents ( patched ) , [
374+ { table_name : "real_table" , feature_count : 2 } ,
375+ ] ) ;
376+ } ) ;
377+
324378 it ( "leaves a complete GeoPackage untouched" , ( ) => {
325379 const original = buildGpkg ( { withOgrContents : true , featureCount : 3 } ) ;
326380 const patched = ensureGpkgFeatureCountSync ( SQL , original ) ;
0 commit comments