From ea06fd66806102fb9790b336afc9770091de3767 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Sun, 27 Sep 2026 08:56:36 +0000
Subject: [PATCH] [#1075] Test the JDBC guards against a neighbouring MySQL database and Oracle schema (#1108)
---
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/MySqlTestCase.java | 123 +++++++++++++++++++++++++++++++++++++++++
1 files changed, 123 insertions(+), 0 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/MySqlTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/MySqlTestCase.java
index a3d325c..edf6fc3 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/MySqlTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/MySqlTestCase.java
@@ -15,12 +15,20 @@
*/
package org.opends.server.backends.jdbc;
+import org.forgerock.opendj.ldap.ByteString;
+import org.opends.server.backends.pluggable.spi.AccessMode;
+import org.opends.server.backends.pluggable.spi.TreeName;
+import org.opends.server.backends.pluggable.spi.WriteOperation;
+import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MySQLContainer;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@@ -174,6 +182,121 @@
throw new AssertionError("an account granted one query an hour must be refused within four connects");
}
+ /** The database of the case below: a directory next to the one of this suite, on the same server. */
+ private static final String NEIGHBOUR = "opendj_neighbour1075";
+
+ @Override
+ protected void dropStaleNeighbours() throws SQLException {
+ grant(getJdbcUrl(), "drop database if exists " + NEIGHBOUR);
+ }
+
+ @DataProvider
+ public Object[][] databaseTerms() {
+ return new Object[][] {
+ // the default of Connector/J: the database is the catalog, and the lookups are asked in it
+ { "catalog", "" },
+ // the database is the schema: the connection names no catalog, the lookups span the server,
+ // and only the schema path read off the connection tells the neighbour apart
+ { "schema", "?databaseTerm=SCHEMA" },
+ };
+ }
+
+ /**
+ * A table and an index of another database of this server answer for none of this backend's, however
+ * the connection names its database (#1075) - the mysql twin of the postgres case of #902.
+ * <p>
+ * A table is named after its tree and an index after its table, so two directories on one server - the
+ * stock backend id in two databases - hold the same table and the same index. Connector/J asked with no
+ * catalog lists the tables of every database, and both guards of {@code openTree()} would then skip a
+ * create this backend needs: the table one leaves every statement addressing a table that is not there,
+ * and the index one - the quiet half - leaves the {@code where k>? order by k} batches of every cursor
+ * a full scan for the life of the deployment.
+ * <p>
+ * Under the default {@code databaseTerm} the guards hold by two layers at once - the catalog they pass
+ * and the database {@code TableScope.covers()} reads off every row - and this case goes red only when
+ * both give way; the unit cases of {@code JDBCStorageRetryTest} pin each of them. Under
+ * {@code databaseTerm=SCHEMA} the connection names no catalog and the schema path is the one layer
+ * there is, which is what this case exists for: what it rests on is how the driver lists a table of
+ * another database, and only a live server answers that.
+ */
+ @Test(dataProvider = "databaseTerms")
+ public void testAnOpenIsAnsweredForByNoTableOfAnotherDatabase(String term, String urlOptions) throws Exception {
+ final TreeName tree = new TreeName("testAnotherDatabase", "tree");
+ final JDBCStorage storage =
+ new JDBCStorage(createBackendCfg(getBackendId() + "_" + term, getJdbcUrl() + urlOptions), null);
+ final String tableName = storage.getTableName(tree);
+ final String indexName = "k_" + tableName.substring("opendj_".length());
+ try {
+ // the neighbouring directory: the same table and the same index, in a database this storage
+ // reaches through no unqualified name of its own. Spelled out rather than opened by a storage,
+ // so that the fixture is the collision and nothing else
+ grant(getJdbcUrl(), "drop database if exists " + NEIGHBOUR,
+ "create database " + NEIGHBOUR,
+ "create table " + NEIGHBOUR + "." + tableName
+ + " (h char(128),k varbinary(255),v longblob,primary key(h,k))",
+ "create index " + indexName + " on " + NEIGHBOUR + "." + tableName + " (k)");
+ assertFalse(isExistsIn(DATABASE, tableName, null),
+ "the case did not start with the table of this backend absent from its database");
+
+ storage.open(AccessMode.READ_WRITE);
+ storage.write(new WriteOperation() {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception {
+ txn.openTree(tree, true);
+ // the destructive half of the table guard, and the reason it is loud: found abroad, the
+ // table is created nowhere and this statement addresses a table that is not there
+ txn.put(tree, ByteString.valueOfUtf8("a key of this backend"),
+ ByteString.valueOfUtf8("a value of this backend"));
+ }
+ });
+
+ assertTrue(isExistsIn(DATABASE, tableName, null),
+ "the open took the table of another database for its own and created none");
+ assertTrue(isExistsIn(DATABASE, tableName, indexName),
+ "the open took the index of another database for its own: the cursor batches of this tree are full scans behind it");
+ assertEquals(rowCountIn(DATABASE, tableName), 1,
+ "the write of this backend landed in a table other than the one the open made");
+ assertEquals(rowCountIn(NEIGHBOUR, tableName), 0,
+ "the write of this backend landed in the table of the neighbouring database");
+ } finally {
+ clearQuietly(storage);
+ grant(getJdbcUrl(), "drop database if exists " + NEIGHBOUR);
+ }
+ }
+
+ /** The database of the connections of this suite. */
+ private static final String DATABASE = "database_name";
+
+ /**
+ * Whether that one database holds the table - or, given an index name, that index of it - asked of
+ * information_schema by name rather than through the catalog lookups under test.
+ */
+ private boolean isExistsIn(String database, String tableName, String indexName) throws SQLException {
+ final String query = indexName == null
+ ? "select 1 from information_schema.tables where table_schema=? and table_name=?"
+ : "select 1 from information_schema.statistics where table_schema=? and table_name=? and index_name=?";
+ try (final Connection con = DriverManager.getConnection(getJdbcUrl());
+ final PreparedStatement st = con.prepareStatement(query)) {
+ st.setString(1, database);
+ st.setString(2, tableName);
+ if (indexName != null) {
+ st.setString(3, indexName);
+ }
+ try (final ResultSet rs = st.executeQuery()) {
+ return rs.next();
+ }
+ }
+ }
+
+ /** What the table of that one database holds, which says which of the two tables a write went to. */
+ private int rowCountIn(String database, String tableName) throws SQLException {
+ try (final Connection con = DriverManager.getConnection(getJdbcUrl());
+ final Statement st = con.createStatement();
+ final ResultSet rs = st.executeQuery("select count(*) from " + database + "." + tableName)) {
+ return rs.next() ? rs.getInt(1) : -1;
+ }
+ }
+
/** The account of the test is made and unmade on the connection of the suite's own credentials. */
private static void grant(String url, String... statements) throws SQLException {
try (final Connection admin = DriverManager.getConnection(url);
--
Gitblit v1.10.0