Skip to content

[#902] Name the table to the index guard of the JDBC backend the way the database stores it - #1001

Open
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:feature/jdbc-902-schema-scoped-index-guard
Open

[#902] Name the table to the index guard of the JDBC backend the way the database stores it#1001
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:feature/jdbc-902-schema-scoped-index-guard

Conversation

@vharseko

@vharseko vharseko commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #902

Problem

#902 reported both catalog reads of the JDBC backend — the one guarding create table and the one guarding create index — asking with null where the JDBC contract takes a schema, so an object of another schema of the same database answered for the one this backend was about to create. A table is named after the hash of its tree name and its index after the table, so both names follow from the configuration and from nothing else: two directories sharing one database, each with a schema and a search_path of its own, hold a table of the same name and an index of the same name, and nothing about either name tells them apart.

Most of that is already fixed on master. #893 gave every lookup of this backend a TableScope — the database of the connection and the schemas an unqualified name of it resolves in — and isExistsTable(), isExistsIndex() and the leftover listing all read through it. That PR never named #902, so the issue stayed open. What it left behind is what this changes.

The table named to getIndexInfo() is not the name the database stores

isExistsIndex() passed the table name as it was written, and the fold an unquoted identifier goes through was applied by one caller alone — the oracle branch of openTree(), with tableName.toUpperCase(Locale.ROOT) at the call site:

// before
if (!isExistsIndex(tableName.toUpperCase(Locale.ROOT),"k_"+tableName.substring("opendj_".length()))) {
...
try (final ResultSet rs = con.getMetaData().getIndexInfo(scope.catalog, null, tableName, false, true)) {

getIndexInfo() takes a name and matches it against the stored form, so the rule belongs in the lookup — where isExistsTable() already keeps it, in storedIdentifier(), which asks the driver which way it folds rather than matching the engine's class name. On the three engines that reach this guard today the two spellings agree; the knowledge sitting in one branch of the caller is what a fourth engine would inherit nothing of — a lookup that finds no index of a table that carries one, and the create index behind it reissued. On mysql and oracle, whose create index has no if not exists, that is the open of the tree failing.

The direction #902 is about had no case

testAClearFindsATableOfAnotherSchemaOfTheSearchPath covers the other half of the narrowing: a table of a schema on the path must still be found, and the open must not create a second one shadowing it. Nothing covered a table or an index of a schema off the path being passed over — the collision the issue was filed about.

Fix

  • isExistsIndex() names the table the way the database stores it, through the same storedIdentifier() the table lookup takes, and the metadata is read once for both.
  • The oracle branch of openTree() no longer folds for itself. Its comment said "unquoted identifiers are stored in uppercase", which is true of that engine and not of the guard's business: the driver is asked instead, exactly as isExistsTable() asks it.

Nothing about the scope of either lookup changes — that is #893's and stays as it is.

Tests

PgSqlTestCase.testAnOpenIsAnsweredForByNoTableOfASchemaOffTheSearchPath — the case of #902. A neighbouring directory is spelled out by hand in a schema this storage's connections do not resolve in: the same table opendj_<hash> and the same index k_<hash>. The open must create both of its own in the schema it works in, and the write of the transaction must land in its own table (the neighbour's row count is asserted to stay zero). The fixture is made by hand rather than by a second storage, because what the case needs is a schema this one does not reach — and a storage of this suite creates its tables in the schema it does.

JDBCStorageRetryTest.testTheIndexGuardNamesTheTableAsTheDatabaseStoresIt — over a mocked engine whose metadata reports that it stores identifiers folded upwards, the guard has to ask about the folded name, whichever branch of openTree() the driver took to get there.

Watched failing

Both were run against the guard put back the way it was, one half at a time:

The guard, un-narrowed What failed
the index lookup (as before #893) the open took the index of a schema it does not reach unqualified for its own: the cursor batches of this tree are full scans behind it — the quiet half of the issue, the table half passing beside it
the table lookup (as before #893) PSQLException: ERROR: relation "opendj_4f91…" does not exist — the loud half, raised by the first statement against a table that was never created
the fold (the code of this PR reverted) getIndexInfo asked "opendj_1809…" where the driver says the database stores "OPENDJ_1809…"

What was run

Suite Result
JDBCStorageRetryTest 68/68
PgSqlTestCase 80/80
MySqlTestCase 78/78
OracleTestCase not run — testcontainers would not bring gvenzl/oracle-free up within its 5 minute startup wait (two attempts), so all 80 cases skipped

Since the oracle branch is the one whose hard-coded fold this PR removes, the assumption it now rests on was probed directly against a hand-started gvenzl/oracle-free:

storesUpperCase     : true
storesLowerCase     : false
getCatalog          : null
getSchema           : OPENDJ
asked as written    : NOT FOUND (opendj_probe902)
asked as stored     : found (OPENDJ_PROBE902 -> K_PROBE902, schema OPENDJ)

The fold is required there, and storedIdentifier() produces exactly the name the call site used to spell out.

…DBC backend the way the database stores it

The catalog reads that guard the create table and the create index of openTree() are narrowed to the database and the schema path of their connection since OpenIdentityPlatform#893, which is what OpenIdentityPlatform#902 asked for. Two things were left over.

isExistsIndex() named the table to getIndexInfo() as it was written, and the fold an unquoted identifier goes through was applied by its oracle caller alone. getIndexInfo() matches its argument against the stored form, so the rule belongs where isExistsTable() already keeps it - storedIdentifier(), which asks the driver rather than the engine name - and the oracle branch no longer carries an upper case of its own.

And the direction OpenIdentityPlatform#902 is about had no case: PgSqlTestCase covered a table of another schema of the search path being found, never a table of a schema off it being passed over. The new case gives a neighbouring directory a table and an index of the same names in a schema this storage does not resolve in, and asserts the open creates both of its own and writes to its own table.
@vharseko vharseko added bug jdbc tests Test suites: fixing, enabling, un-disabling labels Sep 9, 2026
@vharseko
vharseko requested a review from maximthomas September 9, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug jdbc tests Test suites: fixing, enabling, un-disabling

Projects

None yet

1 participant