1919import java .util .LinkedHashMap ;
2020import java .util .List ;
2121import java .util .Map ;
22- import java .util .Objects ;
23- import java .util .logging .Level ;
24- import java .util .logging .Logger ;
2522import javax .sql .DataSource ;
26- import org .geotools .util .logging .Logging ;
27- import org .springframework .dao .DataAccessException ;
28- import org .springframework .jdbc .core .JdbcOperations ;
2923import org .springframework .jdbc .support .JdbcAccessor ;
3024import org .springframework .jdbc .support .JdbcUtils ;
3125import org .springframework .jdbc .support .MetaDataAccessException ;
3832 */
3933public class SQLDialect {
4034
41- private static final Logger LOG = Logging .getLogger (SQLDialect .class );
42-
4335 // size guesses: 128 characters should be more than enough for layer name, the gridset id
4436 // is normally an epsg code so 32 is way more than enough, the blob format
4537 // is normally a mime plus some extras, again 64 should fit, a param id is
@@ -124,85 +116,28 @@ public void initializeTables(String schema, SimpleJdbcTemplate template) {
124116 }
125117 }
126118 }
127- // Bring pre-existing installations up to the current FK contract (ON UPDATE CASCADE).
128- // No-op for fresh schemas created above; no -op for dialects that cannot support it .
119+ // Bring pre-existing installations up to the current per-dialect FK definition (ON UPDATE CASCADE, or
120+ // DEFERRABLE INITIALLY DEFERRED on Oracle). No -op for the fresh schemas created above .
129121 migrateForeignKeys (schema , template );
130122 }
131123
132124 /**
133- * Upgrades the {@code TILEPAGE -> TILESET} foreign key on installations created before this dialect started
134- * declaring it {@code ON UPDATE CASCADE}, so that {@link #getRenameLayerStatement(String, String, String) renaming
135- * a layer} can rewrite {@code TILESET.KEY} without orphaning the dependent {@code TILEPAGE} rows.
136- *
137- * <p>Idempotent: looks up the existing FK via {@link DatabaseMetaData#getImportedKeys}, and only rewrites it when
138- * the current update rule is not {@link DatabaseMetaData#importedKeyCascade}. Dialects that cannot support
139- * {@code ON UPDATE CASCADE} (notably Oracle) override this method to no-op.
125+ * Upgrades the {@code TILEPAGE -> TILESET} foreign key on installations created before this dialect declared its
126+ * current FK definition, letting {@link #getRenameLayerStatement renaming a layer} rewrite {@code TILESET.KEY}
127+ * without orphaning the dependent {@code TILEPAGE} rows. The dialect contributes the two variation points
128+ * ({@link #tilepageFkIsMigrated} and {@link #tilepageFkAddSql}); the crash- and concurrency-safe orchestration
129+ * lives in {@link TilepageForeignKeyMigrator}.
140130 */
141131 protected void migrateForeignKeys (String schema , SimpleJdbcTemplate template ) {
142- DataSource ds = Objects .requireNonNull (((JdbcAccessor ) template .getJdbcOperations ()).getDataSource ());
143- try {
144- JdbcUtils .extractDatabaseMetaData (ds , dbmd -> upgradeTilepageForeignKey (dbmd , schema , template ));
145- } catch (MetaDataAccessException e ) {
146- LOG .log (
147- Level .WARNING ,
148- "Could not migrate TILEPAGE foreign key to ON UPDATE CASCADE; layer renames may leave stale rows" ,
149- e );
150- }
151- }
152-
153- /**
154- * {@link DatabaseMetaDataCallback} body for {@link #migrateForeignKeys}: scans the FKs declared on TILEPAGE and,
155- * for any TILEPAGE -> TILESET FK whose update rule is not {@link DatabaseMetaData#importedKeyCascade}, drops it and
156- * re-adds it with {@code ON UPDATE CASCADE ON DELETE CASCADE}. Idempotent: a FK that already cascades on update is
157- * left untouched.
158- *
159- * <p>Concurrent-startup safe: if another instance races us to the upgrade, our drop/add may throw because the
160- * legacy constraint name no longer exists. In that case we re-check the live FK state and, if the cascade form is
161- * already in place, treat it as a concurrent success rather than a failure.
162- */
163- private Void upgradeTilepageForeignKey (DatabaseMetaData dbmd , String schema , SimpleJdbcTemplate template )
164- throws SQLException {
165-
166- final String tilepageName = resolveTableName (dbmd , schema , "TILEPAGE" );
167- if (tilepageName == null ) {
168- return null ;
169- }
170- final String prefix = schema == null ? "" : schema + "." ;
171- final String prefixedTilepageName = prefix + tilepageName ;
172- try (ResultSet rs = dbmd .getImportedKeys (null , schema , tilepageName )) {
173- while (rs .next ()) {
174- String fkName = rs .getString ("FK_NAME" );
175- if (!isTilepageTilesetFkRow (rs , fkName ) || tilepageFkIsMigrated (rs )) {
176- continue ;
177- }
178- String drop = "ALTER TABLE %s DROP CONSTRAINT %s" .formatted (prefixedTilepageName , fkName );
179- String add = tilepageFkAddSql (prefixedTilepageName , prefix );
180-
181- LOG .info (() -> "Migrating TILEPAGE.TILESET_ID foreign key (was constraint %s)" .formatted (fkName ));
182- JdbcOperations jdbcOperations = template .getJdbcOperations ();
183- try {
184- jdbcOperations .execute (drop );
185- jdbcOperations .execute (add );
186- } catch (DataAccessException raceLikely ) {
187- if (isTilepageFkAlreadyMigrated (dbmd , schema , tilepageName )) {
188- LOG .fine (() -> "TILEPAGE FK was migrated concurrently by another instance "
189- + "while this instance was trying to drop %s; accepting concurrent migration"
190- .formatted (fkName ));
191- return null ;
192- }
193- throw raceLikely ;
194- }
195- }
196- }
197- return null ;
132+ new TilepageForeignKeyMigrator (this ::tilepageFkIsMigrated , this ::tilepageFkAddSql ).migrate (schema , template );
198133 }
199134
200- /** Hook: {@code true} when this dialect's FK row already reflects the migrated shape . Oracle overrides. */
135+ /** Hook: {@code true} when this dialect's FK row is already migrated. Oracle overrides. */
201136 protected boolean tilepageFkIsMigrated (ResultSet rs ) throws SQLException {
202137 return rs .getShort ("UPDATE_RULE" ) == DatabaseMetaData .importedKeyCascade ;
203138 }
204139
205- /** Hook: the {@code ALTER TABLE ... ADD FOREIGN KEY} statement for this dialect's migrated FK shape . */
140+ /** Hook: the {@code ALTER TABLE ... ADD FOREIGN KEY} statement for this dialect's migrated FK. */
206141 protected String tilepageFkAddSql (String prefixedTilepageName , String prefix ) {
207142 return """
208143 ALTER TABLE %s ADD FOREIGN KEY (TILESET_ID)
@@ -212,43 +147,6 @@ protected String tilepageFkAddSql(String prefixedTilepageName, String prefix) {
212147 .formatted (prefixedTilepageName , prefix );
213148 }
214149
215- /** Re-checks FK state after a failed migration attempt to detect a concurrent migration from another instance. */
216- private boolean isTilepageFkAlreadyMigrated (DatabaseMetaData dbmd , String schema , String tilepageName )
217- throws SQLException {
218- try (ResultSet rs = dbmd .getImportedKeys (null , schema , tilepageName )) {
219- while (rs .next ()) {
220- if (isTilepageTilesetFkRow (rs , rs .getString ("FK_NAME" )) && tilepageFkIsMigrated (rs )) {
221- return true ;
222- }
223- }
224- }
225- return false ;
226- }
227-
228- /** Identity-only check: is this metadata row the TILEPAGE -> TILESET(KEY) FK? */
229- private static boolean isTilepageTilesetFkRow (ResultSet rs , String fkName ) throws SQLException {
230- if (fkName == null || fkName .isEmpty ()) {
231- return false ;
232- }
233- String pkTable = rs .getString ("PKTABLE_NAME" );
234- String fkColumn = rs .getString ("FKCOLUMN_NAME" );
235- return "TILESET" .equalsIgnoreCase (pkTable ) && "TILESET_ID" .equalsIgnoreCase (fkColumn );
236- }
237-
238- private static String resolveTableName (DatabaseMetaData dbmd , String schema , String tableName ) throws SQLException {
239- try (ResultSet rs = dbmd .getTables (null , schema , tableName .toLowerCase (), null )) {
240- if (rs .next ()) {
241- return rs .getString ("TABLE_NAME" );
242- }
243- }
244- try (ResultSet rs = dbmd .getTables (null , schema , tableName , null )) {
245- if (rs .next ()) {
246- return rs .getString ("TABLE_NAME" );
247- }
248- }
249- return null ;
250- }
251-
252150 /** Checks if the specified table exists */
253151 private boolean tableExists (SimpleJdbcTemplate template , final String schema , final String tableName ) {
254152 try {
0 commit comments