From 40c239b483e82f359f1527f92aac04c9bb461301 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 20 Aug 2026 08:35:27 +0000
Subject: [PATCH] [#885] Ask the catalog for the table of a tree by name (#886)

---
 opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java    |   51 +++++++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java |   36 ++++++++++++++++--
 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 fcf3737..0987886 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 @@
 		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 @@
 		}
 
 		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_<hash>" also matches a table named "opendjX<hash>"
+						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 472f62f..f7ad4ec 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
@@ -139,6 +139,57 @@
 	}
 
 	/**
+	 * 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<ByteString>() {
+				@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).
 	 */

--
Gitblit v1.10.0