| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2025 3A Systems, LLC. |
| | | * Copyright 2025-2026 3A Systems, LLC. |
| | | */ |
| | | 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.oracle.OracleContainer; |
| | | 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; |
| | | import java.time.Duration; |
| | | |
| | | //docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.assertFalse; |
| | | import static org.testng.Assert.assertTrue; |
| | | |
| | | //docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password -e ORACLE_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart |
| | | |
| | | @Test(sequential = true) |
| | | public class OracleTestCase extends TestCase { |
| | |
| | | |
| | | @Override |
| | | protected String getContainerDockerCommand() { |
| | | return "run before test: docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart"; |
| | | return "run before test: docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password -e ORACLE_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart"; |
| | | } |
| | | |
| | | @Override |
| | |
| | | return "jdbc:oracle:thin:opendj/password@localhost: " + ((container==null)?"1521":container.getMappedPort(1521)) + "/database_name"; |
| | | } |
| | | |
| | | /** The schema of the case below: another user of this database, holding a directory of its own. */ |
| | | private static final String NEIGHBOUR = "opendj_neighbour1075"; |
| | | |
| | | /** |
| | | * The administrator of the database, who makes the neighbour and grants this suite a look at it: the |
| | | * container is given one password for both accounts, and the user of the suite may create no other. |
| | | */ |
| | | private String getSystemJdbcUrl() { |
| | | return getJdbcUrl().replace("opendj/password", "system/password"); |
| | | } |
| | | |
| | | @Override |
| | | protected void dropStaleNeighbours() throws SQLException { |
| | | administer("drop user if exists " + NEIGHBOUR + " cascade"); |
| | | } |
| | | |
| | | /** |
| | | * A table and an index of another schema answer for none of this backend's (#1075) - the oracle twin |
| | | * of the postgres case of #902. |
| | | * <p> |
| | | * A table is named after its tree and an index after its table, so two directories in two schemas of |
| | | * one database hold the same table and the same index. Oracle names no catalog, so a lookup of |
| | | * {@code openTree()} is asked of every schema the user may see, and the schema {@code TableScope} |
| | | * reads off the connection is the one thing telling the neighbour's index from this one's: found |
| | | * abroad, the table guard leaves every statement of the backend addressing a table that is not there, |
| | | * and the index one leaves the {@code where k>? order by k} batches of every cursor a full scan. |
| | | * <p> |
| | | * The grant is the whole of the fixture: the dictionary views the driver reads list another user's |
| | | * table only to a user granted something on it, and without it the lookup finds nothing either way. |
| | | */ |
| | | @Test |
| | | public void testAnOpenIsAnsweredForByNoTableOfAnotherSchema() throws Exception { |
| | | final TreeName tree = new TreeName("testAnotherSchema", "tree"); |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_anotherSchema"), 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 schema 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 |
| | | administer("drop user if exists " + NEIGHBOUR + " cascade", |
| | | "create user " + NEIGHBOUR + " identified by password quota unlimited on users", |
| | | "create table " + NEIGHBOUR + "." + tableName + " (h char(128),k raw(2000),v blob,primary key(h,k))", |
| | | "create index " + NEIGHBOUR + "." + indexName + " on " + NEIGHBOUR + "." + tableName + " (k)", |
| | | "grant select on " + NEIGHBOUR + "." + tableName + " to opendj"); |
| | | assertFalse(isExistsOwn("select 1 from user_tables where table_name=upper(?)", tableName), |
| | | "the case did not start with the table of this backend absent from its schema"); |
| | | |
| | | 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(isExistsOwn("select 1 from user_tables where table_name=upper(?)", tableName), |
| | | "the open took the table of another schema for its own and created none"); |
| | | assertTrue(isExistsOwn("select 1 from user_indexes where index_name=upper(?)", indexName), |
| | | "the open took the index of another schema for its own: the cursor batches of this tree are full scans behind it"); |
| | | assertEquals(rowCount(tableName), 1, |
| | | "the write of this backend landed in a table other than the one the open made"); |
| | | assertEquals(rowCount(NEIGHBOUR + "." + tableName), 0, |
| | | "the write of this backend landed in the table of the neighbouring schema"); |
| | | } finally { |
| | | clearQuietly(storage); |
| | | administer("drop user if exists " + NEIGHBOUR + " cascade"); |
| | | } |
| | | } |
| | | |
| | | /** Runs the given statements as the administrator of the database. */ |
| | | private void administer(String... statements) throws SQLException { |
| | | try (final Connection con = DriverManager.getConnection(getSystemJdbcUrl()); |
| | | final Statement st = con.createStatement()) { |
| | | for (final String statement : statements) { |
| | | st.execute(statement); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Whether the dictionary of the user of this suite lists the named object, asked by name rather than |
| | | * through the catalog lookups under test. |
| | | */ |
| | | private boolean isExistsOwn(String query, String name) throws SQLException { |
| | | try (final Connection con = DriverManager.getConnection(getJdbcUrl()); |
| | | final PreparedStatement st = con.prepareStatement(query)) { |
| | | st.setString(1, name); |
| | | try (final ResultSet rs = st.executeQuery()) { |
| | | return rs.next(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** What the named table holds, read as the administrator, who reaches both schemas. */ |
| | | private int rowCount(String qualified) throws SQLException { |
| | | final String table = qualified.contains(".") ? qualified : "opendj." + qualified; |
| | | try (final Connection con = DriverManager.getConnection(getSystemJdbcUrl()); |
| | | final Statement st = con.createStatement(); |
| | | final ResultSet rs = st.executeQuery("select count(*) from " + table)) { |
| | | return rs.next() ? rs.getInt(1) : -1; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Test(skipFailedInvocations = true) //ORA UPSERT error |
| | | public void test_issue_496_2() { |