| | |
| | | unstampableTrees.clear(); |
| | | } |
| | | |
| | | // The trees this storage has taken an interest in, and the tables they map to. listTrees() - |
| | | // and through it removeStorageFiles() - reads this, so a tree only belongs here once this |
| | | // backend uses it: see toTableName() below for the trees that are merely asked about. |
| | | final LoadingCache<TreeName,String> tree2table = Caffeine.newBuilder() |
| | | .build(treeName -> { |
| | | try { |
| | | final MessageDigest md = MessageDigest.getInstance("SHA-224"); |
| | | final byte[] messageDigest = md.digest(treeName.toString().getBytes()); |
| | | final StringBuilder hashtext = new StringBuilder(56); |
| | | for (byte b : messageDigest) { |
| | | String hex = Integer.toHexString(0xff & b); |
| | | if (hex.length() == 1) hashtext.append('0'); |
| | | hashtext.append(hex); |
| | | } |
| | | return "opendj_" + hashtext; |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new RuntimeException(e); |
| | | .build(JDBCStorage::toTableName); |
| | | |
| | | /** |
| | | * The table a tree name maps to. A pure function of the name, so that a tree can be read |
| | | * without being entered into tree2table: the compressed schema reads the tree its definitions |
| | | * used to be shared under (#873), a tree this backend does not own, and removeStorageFiles() |
| | | * drops every table tree2table names. |
| | | * <p> |
| | | * Which of the two a statement takes therefore says who owns the tree it names: a path that |
| | | * creates or writes one - openTree(), clearTree(), deleteTree(), put(), update(), delete() - |
| | | * takes the enrolling {@link #getTableName(TreeName)}, and a read-only path - read(), |
| | | * getRecordCount(), isExistsTable() and the cursor - takes {@link #readTableName(TreeName)}, |
| | | * which computes this only for a tree that is not enrolled already. Every tree this backend |
| | | * owns passes through openTree(name, true) as it is opened, so listTrees() still names the |
| | | * complete owned set. |
| | | */ |
| | | static String toTableName(TreeName treeName) { |
| | | try { |
| | | final MessageDigest md = MessageDigest.getInstance("SHA-224"); |
| | | final byte[] messageDigest = md.digest(treeName.toString().getBytes()); |
| | | final StringBuilder hashtext = new StringBuilder(56); |
| | | for (byte b : messageDigest) { |
| | | String hex = Integer.toHexString(0xff & b); |
| | | if (hex.length() == 1) hashtext.append('0'); |
| | | hashtext.append(hex); |
| | | } |
| | | }); |
| | | return "opendj_" + hashtext; |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | String getTableName(TreeName treeName) { |
| | | return tree2table.get(treeName); |
| | | } |
| | | |
| | | /** |
| | | * The table a tree name maps to, for a statement that only reads it. Answered from the memo of |
| | | * {@link #getTableName(TreeName)} where the tree is in it, and computed without being put there |
| | | * otherwise. |
| | | * <p> |
| | | * Every tree this backend owns is enrolled as it is opened, so the per-entry read path stays a |
| | | * map lookup: {@link #toTableName(TreeName)} takes a JCA provider lookup and a digest per call, |
| | | * which read() would otherwise pay for every entry of every search. Only a tree this backend |
| | | * does not own - the shared compressed schema tree the migration of #873 reads - is computed, |
| | | * twice per open of the backend. |
| | | */ |
| | | String readTableName(TreeName treeName) { |
| | | final String enrolled=tree2table.getIfPresent(treeName); |
| | | return enrolled!=null ? enrolled : toTableName(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 |
| | |
| | | |
| | | @Override |
| | | public ByteString read(TreeName treeName, ByteSequence key) { |
| | | try (final PreparedStatement statement=con.prepareStatement("select v from "+getTableName(treeName)+" where h="+hashParam(con)+" and k=?")){ |
| | | try (final PreparedStatement statement=con.prepareStatement("select v from "+readTableName(treeName)+" where h="+hashParam(con)+" and k=?")){ |
| | | statement.setString(1,key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2,real2db(key.toByteArray())); |
| | | try(ResultSet rc=executeResultSet(statement)) { |
| | |
| | | |
| | | @Override |
| | | public long getRecordCount(TreeName treeName) { |
| | | try (final PreparedStatement statement=con.prepareStatement("select count(*) from "+getTableName(treeName)); |
| | | try (final PreparedStatement statement=con.prepareStatement("select count(*) from "+readTableName(treeName)); |
| | | final ResultSet rc=executeResultSet(statement)){ |
| | | return rc.next() ? rc.getLong(1) : 0; |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public boolean treeExists(TreeName treeName) { |
| | | return isExistsTable(treeName); |
| | | } |
| | | |
| | | // Readable, not writeable: the caller that asks about a tree this backend does not own is |
| | | // the compressed schema migration (#873), which probes the shared tree from the writeable |
| | | // transaction of RootContainer.open() but must not create or enrol it. Answering that from |
| | | // the readable transaction keeps the probe available to every reader, and costs nothing: |
| | | // the writeable one inherits it. |
| | | boolean isExistsTable(TreeName treeName) { |
| | | final String tableName = readTableName(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) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * A transaction able to write, unless the storage was opened read-only: then it may open an existing tree and |
| | |
| | | return driverName.contains("mysql") || driverName.contains("oracle"); |
| | | } |
| | | |
| | | boolean isExistsTable(TreeName treeName) { |
| | | 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) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | String getTableDialect() { |
| | | if (driverNameOf(con).contains("oracle")) { |
| | | return "h char(128),k raw(2000),v blob,primary key(h,k)"; |
| | |
| | | // repositioning transfer "fetchsize" rows over the network (#860). |
| | | final class CursorImpl implements Cursor<ByteString, ByteString> { |
| | | final Connection con; |
| | | final TreeName treeName; |
| | | final String tableName; |
| | | // the enrolling name, resolved once and only if this cursor ever deletes |
| | | String writeTableName; |
| | | final boolean isReadOnly; |
| | | final int batchSize=Math.max(1,Integer.getInteger("org.openidentityplatform.opendj.jdbc.fetchsize",1000)); |
| | | final int initialBatchSize=Math.min(batchSize,Math.max(1,Integer.getInteger("org.openidentityplatform.opendj.jdbc.fetchsize.initial",32))); |
| | |
| | | public CursorImpl(boolean isReadOnly, Connection con, TreeName treeName) { |
| | | this.isReadOnly=isReadOnly; |
| | | this.con=con; |
| | | this.tableName=getTableName(treeName); |
| | | this.treeName=treeName; |
| | | // the read statements below take the non-enrolling name: a cursor is how the migration |
| | | // of #873 reads the shared tree, and reading a tree must not put it up for removal |
| | | this.tableName=readTableName(treeName); |
| | | this.limitClause=((CachedConnection)con).parent.getClass().getName().contains("mysql") |
| | | ? " limit ?,?" : " offset ? rows fetch next ? rows only"; |
| | | } |
| | |
| | | if (isReadOnly) { |
| | | throw new UnsupportedOperationException(); |
| | | } |
| | | try (final PreparedStatement statement=con.prepareStatement("delete from "+tableName+" where h="+hashParam(con)+" and k=?")){ |
| | | if (writeTableName==null) { |
| | | // the enrolling name, unlike the read statements above: this writes to the tree, so |
| | | // it is one this backend owns, and removeStorageFiles() has to know about it |
| | | writeTableName=getTableName(treeName); |
| | | } |
| | | try (final PreparedStatement statement=con.prepareStatement("delete from "+writeTableName+" where h="+hashParam(con)+" and k=?")){ |
| | | statement.setString(1,key2hash.get(ByteBuffer.wrap(db2real(currentKeyDb)))); |
| | | statement.setBytes(2,currentKeyDb); |
| | | execute(statement); |