From 89add4ad3c9f8cea304d545440454416b30682ab Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 19 Aug 2026 18:49:33 +0300 Subject: [PATCH] [#885] Ask the catalog for the table of a tree by name isExistsTable() listed every table of the database and looked for a match in Java: no catalog filter, no schema filter, no table name. It is called from openTree(createOnDemand) through AbstractTree.open() for every tree of the backend - about 25 of them for a stock suffix - so opening a backend walked the whole catalog once per tree, on a database this backend may well be sharing with something else. The name is passed as the pattern now. It has to be passed in the form the catalog stores it, since an unquoted identifier is folded when it is stored - to upper case on oracle, to lower case on postgresql - and a metadata pattern is matched against the stored form rather than against the name as it was written. Which way a driver folds is asked of the driver itself, through the two methods the JDBC contract exposes for it, rather than by matching its class name as the dialect switches elsewhere in this class do. The comparison of the name is kept: "_" is a single-character wildcard in a metadata pattern, so "opendj_" also matches a table called "opendjX", and the result of the lookup is still verified rather than trusted. testTableOfATreeIsFoundByName covers both answers on every dialect: a second open has to find the table rather than create it again - getting the folding wrong would raise ORA-00955 on oracle - and a second delete has to find nothing rather than drop what is not there. --- .../server/backends/jdbc/JDBCStorage.java | 36 +++++++++++-- .../opends/server/backends/jdbc/TestCase.java | 51 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java index fcf3737df4..09878862b3 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java @@ -134,6 +134,23 @@ String getTableName(TreeName treeName) { return tree2table.get(treeName); } + /** + * The form a catalog pattern has to take to match an identifier this backend created unquoted. + * An unquoted identifier is folded when it is stored - to upper case on oracle, to lower case + * on postgresql - and a metadata pattern is matched against the stored form, not against the + * name as it was written. The driver is asked which way it folds, rather than its class name + * being matched, since this is what the JDBC contract exposes these two methods for. + */ + static String storedIdentifier(DatabaseMetaData metaData, String name) throws SQLException { + if (metaData.storesUpperCaseIdentifiers()) { + return name.toUpperCase(); + } + if (metaData.storesLowerCaseIdentifiers()) { + return name.toLowerCase(); + } + return name; + } + @Override public void removeStorageFiles() throws StorageRuntimeException { final boolean isOpen=getStorageStatus().isWorking(); @@ -266,10 +283,21 @@ public WriteableTransactionTransactionImpl(Connection con) { } boolean isExistsTable(TreeName treeName) { - try (final ResultSet rs = con.getMetaData().getTables(null, null, null, new String[]{"TABLE"})) { - while (rs.next()) { - if (tree2table.get(treeName).equalsIgnoreCase(rs.getString("TABLE_NAME"))) { - return true; + final String tableName = getTableName(treeName); + try { + final DatabaseMetaData metaData = con.getMetaData(); + // asked of the catalog by name: openTree(createOnDemand) calls this for every tree + // of the backend - about 25 of them for a stock suffix, on every open - and listing + // every table of the database each time costs the whole catalog once per tree, on a + // database this backend may well be sharing with something else + try (final ResultSet rs = metaData.getTables(null, null, + storedIdentifier(metaData, tableName), new String[]{"TABLE"})) { + while (rs.next()) { + // the name still has to be compared: "_" is a single-character wildcard in a + // metadata pattern, so "opendj_" also matches a table named "opendjX" + if (tableName.equalsIgnoreCase(rs.getString("TABLE_NAME"))) { + return true; + } } } } catch (Exception e) { diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java index 472f62fc9a..f7ad4ec14d 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java @@ -138,6 +138,57 @@ private static ByteString value(int i) { return ByteString.valueOfUtf8("value" + i); } + /** + * openTree() and deleteTree() ask the catalog whether the table of a tree is there. The name is + * looked up in the form the catalog stores it - an unquoted identifier is folded to upper case + * on oracle and to lower case on postgresql - so getting that wrong makes a second open try to + * create a table that is already there, and a second delete drop one that is already gone (#885). + */ + @Test + public void testTableOfATreeIsFoundByName() throws Exception { + final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); + final TreeName tree = new TreeName("testCatalogLookup", "tree"); + try { + storage.open(AccessMode.READ_WRITE); + storage.write(new WriteOperation() { + @Override + public void run(WriteableTransaction txn) throws Exception { + txn.openTree(tree, true); + txn.put(tree, key(1), value(1)); + } + }); + // the table is there now: opening the tree again must find it, not create it a second time + storage.write(new WriteOperation() { + @Override + public void run(WriteableTransaction txn) throws Exception { + txn.openTree(tree, true); + } + }); + assertEquals(storage.read(new ReadOperation() { + @Override + public ByteString run(ReadableTransaction txn) throws Exception { + return txn.read(tree, key(1)); + } + }), value(1)); + + storage.write(new WriteOperation() { + @Override + public void run(WriteableTransaction txn) throws Exception { + txn.deleteTree(tree); + } + }); + // and gone now: deleting it again must find nothing rather than drop what is not there + storage.write(new WriteOperation() { + @Override + public void run(WriteableTransaction txn) throws Exception { + txn.deleteTree(tree); + } + }); + } finally { + storage.close(); + } + } + /** * Forward repositioning inside the already-fetched batch must be served from the buffer without SQL, * and batch sizes must grow from "fetchsize.initial" to "fetchsize" on sequential reads (#860).