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/JDBCStorageRetryTest.java |   85 ++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 85 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java
index 45b0ebb..14dc2cc 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java
@@ -1136,6 +1136,91 @@
   }
 
   /**
+   * On mysql the guards of {@code openTree()} ask in the database of the connection. A table name carries no
+   * database, so two directories on one server - the stock backend id in two databases - hold the same table
+   * and the same index, and Connector/J reads a null catalog as "any database": asked that way, the index of
+   * the neighbour answers for this one and the create behind it is skipped for good, leaving every
+   * {@code where k>? order by k} batch of every cursor a full scan (#1075). The catalog passed here is one of
+   * two layers - {@code TableScope.covers()} reads the database of every row besides, as the case below pins -
+   * and a live server cannot tell a loss of either one from their both holding, since each covers for the
+   * other: what is pinned here is the question put to the driver.
+   */
+  @Test
+  public void testTheMySqlGuardsAskInTheDatabaseOfTheConnection() throws Exception
+  {
+    final JDBCStorage storage = storageOverAnEngine(mysqlConnection.class, true);
+    when(engineConnection.getCatalog()).thenReturn(THIS_DATABASE);
+    final DatabaseMetaData metaData = engineConnection.getMetaData();
+
+    storage.write(txn -> txn.openTree(TREE, true));
+
+    verify(metaData, atLeastOnce()).getTables(eq(THIS_DATABASE), any(), eq(storage.getTableName(TREE)), any());
+    verify(metaData).getIndexInfo(eq(THIS_DATABASE), any(), eq(storage.getTableName(TREE)), anyBoolean(),
+        anyBoolean());
+  }
+
+  /**
+   * The other layer of the two: an index a driver reports in another database is not this backend's, however
+   * it came to be listed - a driver ignoring the catalog it is given, or a caller no longer passing one. Found
+   * there, it must leave the create of this database's own index to go ahead (#1075).
+   */
+  @Test
+  public void testAnIndexOfAnotherDatabaseAnswersForNoneOfThisOne() throws Exception
+  {
+    final JDBCStorage storage = storageOverAnEngine(mysqlConnection.class, true);
+    when(engineConnection.getCatalog()).thenReturn(THIS_DATABASE);
+    answerTheIndexFrom(engineConnection.getMetaData(), storage, NEIGHBOUR_DATABASE, null);
+
+    storage.write(txn -> txn.openTree(TREE, true));
+
+    verify(engineConnection).prepareStatement(startsWith("create index k_"));
+  }
+
+  /**
+   * And the same under {@code databaseTerm=SCHEMA}, the setting of Connector/J that names the database a
+   * schema: the connection then names no catalog, the lookup is asked of the whole server, and every row comes
+   * back under the catalog {@code def} with the database in its schema - measured against mysql 9.2 with the
+   * Connector/J this backend ships (#1075). The schema path {@code TableScope} reads off the connection is the
+   * only thing left to tell the neighbour's index from this one's; {@code MySqlTestCase} pins how the driver
+   * lists such a row against a live server.
+   */
+  @Test
+  public void testUnderDatabaseTermSchemaAnIndexOfAnotherDatabaseAnswersForNoneOfThisOne() throws Exception
+  {
+    final JDBCStorage storage = storageOverAnEngine(mysqlConnection.class, true);
+    when(engineConnection.getSchema()).thenReturn(THIS_DATABASE);
+    answerTheIndexFrom(engineConnection.getMetaData(), storage, "def", NEIGHBOUR_DATABASE);
+
+    storage.write(txn -> txn.openTree(TREE, true));
+
+    verify(engineConnection).prepareStatement(startsWith("create index k_"));
+  }
+
+  /** The database of the connection of the three cases above, and that of the directory next to it. */
+  private static final String THIS_DATABASE = "this_directory";
+  private static final String NEIGHBOUR_DATABASE = "neighbour_directory";
+
+  /**
+   * Has the index lookup of the fixture report the {@code k_} index of the table of {@link #TREE} as the one of
+   * the given catalog and schema, the way a driver lists the index of another database of the server.
+   */
+  private static void answerTheIndexFrom(DatabaseMetaData metaData, JDBCStorage storage, String catalog,
+      String schema) throws SQLException
+  {
+    final String tableName = storage.getTableName(TREE);
+    // stubbed over the answer of the fixture with doAnswer(): when() would call that answer, which stubs a
+    // result set of its own in the middle of this stubbing
+    doAnswer(invocation -> {
+      final ResultSet indexes = mock(ResultSet.class);
+      when(indexes.next()).thenReturn(true, false);
+      when(indexes.getString("INDEX_NAME")).thenReturn("k_" + tableName.substring("opendj_".length()));
+      when(indexes.getString("TABLE_CAT")).thenReturn(catalog);
+      when(indexes.getString("TABLE_SCHEM")).thenReturn(schema);
+      return indexes;
+    }).when(metaData).getIndexInfo(any(), any(), any(), anyBoolean(), anyBoolean());
+  }
+
+  /**
    * postgresql runs DDL inside the transaction, so a create index the engine rolled back has committed nothing:
    * {@code write()} rolls the attempt back whole and replays it. Raising the flag in front of the statement -
    * which is what mysql and oracle need, since they commit before a DDL of their own accord - would turn a

--
Gitblit v1.10.0