Renaming a layer in a GeoServer + GeoWebCache deployment using the JDBC quota store leaves the quota tables in a half-renamed state: the
LAYER_NAME column reflects the new name, but the KEY column on TILESET and the TILESET_ID column on TILEPAGE still carry the old layer-name prefix. Subsequent quota operations don't find the row by KEY and insert a duplicate, splitting accounting between two rows.
Steps to reproduce
-
Create a tile layer pmtiles:europe. Seed some tiles so the quota
store creates TILESET and TILEPAGE rows. The DB looks like:
SELECT key, layer_name FROM tileset;
key | layer_name
------------------------------------+----------------
pmtiles:europe#EPSG:4326#image/png | pmtiles:europe
pmtiles:europe#EPSG:900913#image/png | pmtiles:europe
SELECT DISTINCT tileset_id FROM tilepage;
tileset_id
------------------------------------
pmtiles:europe#EPSG:4326#image/png
pmtiles:europe#EPSG:900913#image/png
-
Rename the layer's workspace from pmtiles to pmtiles_renamed (or rename the layer directly). The prefixed layer name goes from
pmtiles:europe to pmtiles_renamed:europe. The catalog change fires GWC.layerRenamed("pmtiles:europe", "pmtiles_renamed:europe") -> BlobStoreListener.layerRenamed -> JDBCQuotaStore.renameLayer("pmtiles:europe", "pmtiles_renamed:europe").
-
Expected DB state after the rename:
SELECT key, layer_name FROM tileset;
key | layer_name
--------------------------------------------+-------------------------
pmtiles_renamed:europe#EPSG:4326#image/png | pmtiles_renamed:europe
pmtiles_renamed:europe#EPSG:900913#image/png| pmtiles_renamed:europe
SELECT DISTINCT tileset_id FROM tilepage;
tileset_id
--------------------------------------------
pmtiles_renamed:europe#EPSG:4326#image/png
pmtiles_renamed:europe#EPSG:900913#image/png
-
Actual DB state after the rename:
SELECT key, layer_name FROM tileset;
key | layer_name
----------------------------------------+-------------------------
pmtiles:europe#EPSG:4326#image/png | pmtiles_renamed:europe <- KEY not updated
pmtiles:europe#EPSG:900913#image/png | pmtiles_renamed:europe <- KEY not updated
SELECT DISTINCT tileset_id FROM tilepage;
tileset_id
------------------------------------
pmtiles:europe#EPSG:4326#image/png <- still references the old KEY
pmtiles:europe#EPSG:900913#image/png
LAYER_NAME is updated to the new prefix. TILESET.KEY and TILEPAGE.TILESET_ID still carry the old prefix.
Why it matters
getTileSetById(<new-prefix>#<gridset>#<format>) returns null.
- The next
addToQuotaAndTileCounts for the renamed layer goes through getOrCreateTileSet, doesn't find a row by the new KEY, and inserts a duplicate row. New TILEPAGE rows are written under the new key.
Old rows linger under the stale key and keep accounting bytes against the now-orphaned tileset.
- Quota lookup by id is split across two rows; quota lookup by layer name still works (it's the column we do update).
- The existing tests
JDBCQuotaStoreTest.testRenameLayer and testRenameLayer2 only check rows via LAYER_NAME (countTileSetsByLayerName), so the bug is invisible to current CI.
Root cause
SQLDialect.getRenameLayerStatement at
geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/SQLDialect.java:
public String getRenameLayerStatement(String schema, String oldLayerName, String newLayerName) {
StringBuilder sb = new StringBuilder("UPDATE ");
if (schema != null) sb.append(schema).append(".");
sb.append("TILESET SET LAYER_NAME = :").append(newLayerName)
.append(" WHERE LAYER_NAME = :").append(oldLayerName);
return sb.toString();
}
KEY is built as <layerName>#<gridsetId>#<blobFormat>[#<parametersId>] and is never touched. The TILEPAGE FK is REFERENCES TILESET(KEY) ON DELETE CASCADE - no ON UPDATE CASCADE, so even adding a KEY update needs the cascade (or a second SQL statement).
Proposed fix
Two changes in SQLDialect plus a per-dialect audit:
-
Add ON UPDATE CASCADE to the TILEPAGE -> TILESET FK in TABLE_CREATION_MAP. Emit an idempotent ALTER TABLE ... DROP CONSTRAINT ... ADD CONSTRAINT ... ON UPDATE CASCADE ON DELETE CASCADE from JDBCQuotaStore.initialize() so existing installations pick up the cascade on next startup.
-
Rewrite getRenameLayerStatement to also update KEY:
sb.append("TILESET SET LAYER_NAME = :").append(newLayerName);
sb.append(", KEY = :").append(newLayerName)
.append(" || SUBSTRING(KEY FROM POSITION('#' IN KEY))");
sb.append(" WHERE LAYER_NAME = :").append(oldLayerName);
SUBSTRING(KEY FROM POSITION('#' IN KEY)) is standard SQL on Postgres, H2, HSQL. Oracle needs an override using SUBSTR(KEY, INSTR(KEY, '#')).
-
Extend JDBCQuotaStoreTest.testRenameLayer and testRenameLayer2 to also assert that TILESET.KEY and TILEPAGE.TILESET_ID carry the new prefix.
Affected versions
Present on main and on the 1.28.x series (and probably every release that has the JDBC quota store). Backport target: 1.28.x (consumed by GeoServer 2.28.x).
Out of scope
The wider clustered-deployment concurrency story (SERIALIZABLE isolation + tolerant loops causing CannotSerializeTransactionException) is tracked in
Renaming a layer in a GeoServer + GeoWebCache deployment using the JDBC quota store leaves the quota tables in a half-renamed state: the
LAYER_NAMEcolumn reflects the new name, but theKEYcolumn onTILESETand theTILESET_IDcolumn onTILEPAGEstill carry the old layer-name prefix. Subsequent quota operations don't find the row by KEY and insert a duplicate, splitting accounting between two rows.Steps to reproduce
Create a tile layer
pmtiles:europe. Seed some tiles so the quotastore creates
TILESETandTILEPAGErows. The DB looks like:Rename the layer's workspace from
pmtilestopmtiles_renamed(or rename the layer directly). The prefixed layer name goes frompmtiles:europetopmtiles_renamed:europe. The catalog change firesGWC.layerRenamed("pmtiles:europe", "pmtiles_renamed:europe")->BlobStoreListener.layerRenamed->JDBCQuotaStore.renameLayer("pmtiles:europe", "pmtiles_renamed:europe").Expected DB state after the rename:
Actual DB state after the rename:
LAYER_NAMEis updated to the new prefix.TILESET.KEYandTILEPAGE.TILESET_IDstill carry the old prefix.Why it matters
getTileSetById(<new-prefix>#<gridset>#<format>)returns null.addToQuotaAndTileCountsfor the renamed layer goes throughgetOrCreateTileSet, doesn't find a row by the new KEY, and inserts a duplicate row. NewTILEPAGErows are written under the new key.Old rows linger under the stale key and keep accounting bytes against the now-orphaned tileset.
JDBCQuotaStoreTest.testRenameLayerandtestRenameLayer2only check rows viaLAYER_NAME(countTileSetsByLayerName), so the bug is invisible to current CI.Root cause
SQLDialect.getRenameLayerStatementatgeowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/SQLDialect.java:KEYis built as<layerName>#<gridsetId>#<blobFormat>[#<parametersId>]and is never touched. The TILEPAGE FK isREFERENCES TILESET(KEY) ON DELETE CASCADE- noON UPDATE CASCADE, so even adding aKEYupdate needs the cascade (or a second SQL statement).Proposed fix
Two changes in
SQLDialectplus a per-dialect audit:Add
ON UPDATE CASCADEto the TILEPAGE -> TILESET FK inTABLE_CREATION_MAP. Emit an idempotentALTER TABLE ... DROP CONSTRAINT ... ADD CONSTRAINT ... ON UPDATE CASCADE ON DELETE CASCADEfromJDBCQuotaStore.initialize()so existing installations pick up the cascade on next startup.Rewrite
getRenameLayerStatementto also updateKEY:SUBSTRING(KEY FROM POSITION('#' IN KEY))is standard SQL on Postgres, H2, HSQL. Oracle needs an override usingSUBSTR(KEY, INSTR(KEY, '#')).Extend
JDBCQuotaStoreTest.testRenameLayerandtestRenameLayer2to also assert thatTILESET.KEYandTILEPAGE.TILESET_IDcarry the new prefix.Affected versions
Present on
mainand on the1.28.xseries (and probably every release that has the JDBC quota store). Backport target:1.28.x(consumed by GeoServer 2.28.x).Out of scope
The wider clustered-deployment concurrency story (SERIALIZABLE isolation + tolerant loops causing
CannotSerializeTransactionException) is tracked in