| | |
| | | } |
| | | |
| | | /** |
| | | * treeExists() has to answer for a table that was never created rather than fail: it is how the |
| | | * compressed schema tells a backend with nothing to migrate from one whose definitions are still |
| | | * under the shared prefix (#873), and every other statement of this storage fails outright on a |
| | | * table that does not exist. |
| | | */ |
| | | @Test |
| | | public void testTreeExistsAnswersForAMissingTable() throws Exception { |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); |
| | | final TreeName present = new TreeName("testTreeExists", "present"); |
| | | final TreeName absent = new TreeName("testTreeExists", "absent"); |
| | | try { |
| | | storage.open(AccessMode.READ_WRITE); |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.openTree(present, true); |
| | | assertTrue(txn.treeExists(present)); |
| | | assertFalse(txn.treeExists(absent)); |
| | | } |
| | | }); |
| | | // the read path has to answer as well: export-ldif and verify-index open read-only, where |
| | | // no tree is created and the question cannot be settled by writing one |
| | | storage.read(new ReadOperation<Void>() { |
| | | @Override |
| | | public Void run(ReadableTransaction txn) throws Exception { |
| | | assertTrue(txn.treeExists(present)); |
| | | assertFalse(txn.treeExists(absent)); |
| | | return null; |
| | | } |
| | | }); |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(present); |
| | | assertFalse(txn.treeExists(present)); |
| | | } |
| | | }); |
| | | } finally { |
| | | try { |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(present); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) {} |
| | | storage.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The compressed schema definitions of this backend must live in a table of its own. The tree |
| | | * name they used to carry held no backend qualifier, so its table name was a constant that every |
| | | * JDBC backend of every server sharing the database mapped to, and two of them overwrote each |
| | | * other's token definitions there (#873). The backend of this suite has been opened and populated |
| | | * by PluggableBackendImplTestCase#setUp, so its own table exists by now. |
| | | */ |
| | | @Test |
| | | public void testCompressedSchemaTableIsQualifiedByBackendId() throws Exception { |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); |
| | | try { |
| | | storage.open(AccessMode.READ_WRITE); |
| | | final String shared = JDBCStorage.toTableName(new TreeName("compressed_schema", "compressed_attributes")); |
| | | final String own = JDBCStorage.toTableName( |
| | | new TreeName("compressed_schema_" + getBackendId(), "compressed_attributes")); |
| | | assertFalse(shared.equals(own), "the qualified tree name must map to a table of its own"); |
| | | try (final Connection con = DriverManager.getConnection(getJdbcUrl())) { |
| | | assertTrue(isExistingTable(con, own), own + " (this backend's own definitions) is missing"); |
| | | assertFalse(isExistingTable(con, shared), shared + " is the table every backend used to share"); |
| | | } |
| | | } finally { |
| | | storage.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Reading a tree must not enrol it in the storage's tree map: removeStorageFiles() drops every |
| | | * table that map names, and the compressed schema reads the tree its definitions used to be |
| | | * shared under - which on a shared database is another backend's to keep (#873). Asking whether |
| | | * the tree is there is only the first of those reads: the migration counts it and copies it out |
| | | * too, so one guarded statement would not be enough. |
| | | */ |
| | | @Test |
| | | public void testProbingATreeDoesNotPutItUpForRemoval() throws Exception { |
| | | final TreeName foreign = new TreeName("testProbe", "foreign"); |
| | | final JDBCStorage owner = new JDBCStorage(createBackendCfg(), null); |
| | | owner.open(AccessMode.READ_WRITE); |
| | | owner.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.openTree(foreign, true); |
| | | txn.put(foreign, key(1), value(1)); |
| | | } |
| | | }); |
| | | owner.close(); |
| | | |
| | | // a second storage on the same database, which never opened that tree - the shape of two |
| | | // backends addressing one database |
| | | final JDBCStorage other = new JDBCStorage(createBackendCfg(), null); |
| | | try { |
| | | other.open(AccessMode.READ_WRITE); |
| | | other.read(new ReadOperation<Void>() { |
| | | @Override |
| | | public Void run(ReadableTransaction txn) throws Exception { |
| | | // every read the compressed schema runs against a tree it does not own: it asks |
| | | // whether the tree is there, counts it, reads a key of it and walks it (#873) |
| | | assertTrue(txn.treeExists(foreign)); |
| | | assertEquals(txn.getRecordCount(foreign), 1); |
| | | assertEquals(txn.read(foreign, key(1)), value(1)); |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(foreign)) { |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(1)); |
| | | } |
| | | return null; |
| | | } |
| | | }); |
| | | assertFalse(other.listTrees().contains(foreign), "a tree only read must not be listed for removal"); |
| | | |
| | | other.removeStorageFiles(); |
| | | |
| | | try (final Connection con = DriverManager.getConnection(getJdbcUrl())) { |
| | | assertTrue(isExistingTable(con, JDBCStorage.toTableName(foreign)), |
| | | "clearing one backend dropped a table it had only asked about"); |
| | | } |
| | | } finally { |
| | | other.close(); |
| | | final JDBCStorage cleanup = new JDBCStorage(createBackendCfg(), null); |
| | | try { |
| | | cleanup.open(AccessMode.READ_WRITE); |
| | | cleanup.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(foreign); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) { |
| | | } finally { |
| | | cleanup.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The other side of the same rule: a cursor reads through the non-enrolling name, but deleting |
| | | * through one writes to the tree, so it is a tree this backend owns and removeStorageFiles() |
| | | * has to be able to name it. |
| | | */ |
| | | @Test |
| | | public void testDeletingThroughACursorPutsTheTreeUpForRemoval() throws Exception { |
| | | final TreeName tree = new TreeName("testCursorDelete", "tree"); |
| | | final JDBCStorage owner = new JDBCStorage(createBackendCfg(), null); |
| | | owner.open(AccessMode.READ_WRITE); |
| | | owner.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.openTree(tree, true); |
| | | txn.put(tree, key(1), value(1)); |
| | | } |
| | | }); |
| | | owner.close(); |
| | | |
| | | // a storage that never opened that tree, so nothing but the delete can enrol it |
| | | final JDBCStorage other = new JDBCStorage(createBackendCfg(), null); |
| | | try { |
| | | other.open(AccessMode.READ_WRITE); |
| | | other.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) { |
| | | assertTrue(cursor.next()); |
| | | cursor.delete(); |
| | | } |
| | | } |
| | | }); |
| | | assertTrue(other.listTrees().contains(tree), "a tree written through a cursor must be listed for removal"); |
| | | } finally { |
| | | other.close(); |
| | | final JDBCStorage cleanup = new JDBCStorage(createBackendCfg(), null); |
| | | try { |
| | | cleanup.open(AccessMode.READ_WRITE); |
| | | cleanup.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(tree); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) { |
| | | } finally { |
| | | cleanup.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static boolean isExistingTable(Connection con, String tableName) throws SQLException { |
| | | try (final ResultSet rs = con.getMetaData().getTables(null, null, null, new String[]{"TABLE"})) { |
| | | while (rs.next()) { |
| | | if (tableName.equalsIgnoreCase(rs.getString("TABLE_NAME"))) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 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). |
| | | */ |