The JDBC backend asks the catalog whether its table and its index exist, and narrows neither question by schema. Both reads pass null where the JDBC contract takes a schema — "null means that the schema name should not be used to narrow the search" — so an object of another schema of the same database answers the question, and the object the backend was about to create is never created.
try (final ResultSet rs = metaData.getTables(null, null,
storedIdentifier(metaData, tableName), new String[]{"TABLE"})) {
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:1069-1070 (isExistsTable)
try (final ResultSet rs = con.getMetaData().getIndexInfo(null, null, tableName, false, true)) {
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:1150 (isExistsIndex)
Identifier folding is not the concern here — storedIdentifier already covers it, and the names are 63-character lowercase hex with no truncation. What is missing is the schema.
Why two schemas hold the same name
The table name is a hash of the tree name — getTableName is "opendj_" plus a digest of <baseDN>/<treeName> — so it is a pure function of the configuration, not of the instance. Two OpenDJ instances sharing one database, each with its own schema and its own search_path (a routine way to host two directories on one server), hold a table of the same name and an index of the same name in two different schemas. Nothing about the names distinguishes them.
What each guard then does
isExistsIndex — the guard around the create index of every engine. Backend B finds A's k_<hash> in A's schema and skips the create. B's table keeps only its (h,k) primary key, and the where k>? order by k batches of CursorImpl — the access path that index exists for — run unindexed for the life of the deployment. Nothing fails and nothing is logged: the symptom is a search that reads the whole table.
isExistsTable — the guard around the create table. Backend B finds A's table and skips the create, then issues its first statement against a table that does not exist in its own schema. That one is loud (relation "opendj_<hash>" does not exist on postgresql), but the backend does not open.
The single-instance deployment is unaffected: one schema, one answer.
What a fix has to deal with
Passing the connection's own schema to both calls is the shape of it, but not a one-liner:
Connection.getSchema() is JDBC 4.1 and not every driver answers it usefully — oracle returns the login user, and a driver that does not support it throws SQLFeatureNotSupportedException.
- The schema of
getTables is a pattern and the one of getIndexInfo is a plain name; an unquoted name is stored folded, so it needs storedIdentifier around it exactly as the table name does.
- mysql has no schema separate from its database: Connector/J reports the database as the catalog, and
nullCatalogMeansCurrent decides what null means there. The right narrowing is per engine, which is why this deserves a change of its own rather than a line in another PR.
- A deployment whose account cannot see other schemas is already narrow by privilege, so whatever is done must not make the common case ask for more than it does today.
Provenance
Found while reviewing #883, which changes neither call and only adds a second caller of isExistsIndex (the postgres branch of openTree, which now guards its create index if not exists to keep the transaction replayable). Pre-existing on master; both guards were introduced with the index and the catalog reads themselves.
The JDBC backend asks the catalog whether its table and its index exist, and narrows neither question by schema. Both reads pass
nullwhere the JDBC contract takes a schema — "null means that the schema name should not be used to narrow the search" — so an object of another schema of the same database answers the question, and the object the backend was about to create is never created.opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:1069-1070(isExistsTable)opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:1150(isExistsIndex)Identifier folding is not the concern here —
storedIdentifieralready covers it, and the names are 63-character lowercase hex with no truncation. What is missing is the schema.Why two schemas hold the same name
The table name is a hash of the tree name —
getTableNameis"opendj_"plus a digest of<baseDN>/<treeName>— so it is a pure function of the configuration, not of the instance. Two OpenDJ instances sharing one database, each with its own schema and its ownsearch_path(a routine way to host two directories on one server), hold a table of the same name and an index of the same name in two different schemas. Nothing about the names distinguishes them.What each guard then does
isExistsIndex— the guard around thecreate indexof every engine. Backend B finds A'sk_<hash>in A's schema and skips the create. B's table keeps only its(h,k)primary key, and thewhere k>? order by kbatches ofCursorImpl— the access path that index exists for — run unindexed for the life of the deployment. Nothing fails and nothing is logged: the symptom is a search that reads the whole table.isExistsTable— the guard around thecreate table. Backend B finds A's table and skips the create, then issues its first statement against a table that does not exist in its own schema. That one is loud (relation "opendj_<hash>" does not existon postgresql), but the backend does not open.The single-instance deployment is unaffected: one schema, one answer.
What a fix has to deal with
Passing the connection's own schema to both calls is the shape of it, but not a one-liner:
Connection.getSchema()is JDBC 4.1 and not every driver answers it usefully — oracle returns the login user, and a driver that does not support it throwsSQLFeatureNotSupportedException.getTablesis a pattern and the one ofgetIndexInfois a plain name; an unquoted name is stored folded, so it needsstoredIdentifieraround it exactly as the table name does.nullCatalogMeansCurrentdecides whatnullmeans there. The right narrowing is per engine, which is why this deserves a change of its own rather than a line in another PR.Provenance
Found while reviewing #883, which changes neither call and only adds a second caller of
isExistsIndex(the postgres branch ofopenTree, which now guards itscreate index if not existsto keep the transaction replayable). Pre-existing on master; both guards were introduced with the index and the catalog reads themselves.