ninja.leaping.permissionsex.backend.sql.SqlDao.java Source code

Java tutorial

Introduction

Here is the source code for ninja.leaping.permissionsex.backend.sql.SqlDao.java

Source

/**
 * PermissionsEx
 * Copyright (C) zml and PermissionsEx contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ninja.leaping.permissionsex.backend.sql;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import ninja.leaping.permissionsex.backend.sql.dao.LegacyDao;
import ninja.leaping.permissionsex.backend.sql.dao.LegacyMigration;
import ninja.leaping.permissionsex.rank.RankLadder;
import ninja.leaping.permissionsex.util.ThrowingSupplier;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;

/**
 * Abstraction to communicate with the SQL database. Instances are not thread-safe -- it's best to create a new one for each operation on a single thread
 */
public abstract class SqlDao implements AutoCloseable {
    private final Connection conn;
    private final SqlDataStore ds;
    int holdOpen, transactionLevel;

    public SqlDao(SqlDataStore ds) throws SQLException {
        this.ds = ds;
        this.conn = ds.getDataSource().getConnection();
    }

    // -- Queries

    protected String getSelectGlobalParameterQuery() {
        return "SELECT (`value`) FROM {}global WHERE `key`=?";
    }

    protected abstract String getInsertGlobalParameterQueryUpdating();

    protected String getDeleteGlobalParameterQuery() {
        return "DELETE FROM {}global WHERE `key`=?";
    }

    protected String getGetSubjectRefIdQuery() {
        return "SELECT type, identifier FROM {}subjects WHERE id=?";
    }

    protected String getGetSubjectRefTypeNameQuery() {
        return "SELECT id FROM {}subjects WHERE type=? AND identifier=?";
    }

    protected String getDeleteSubjectIdQuery() {
        return "DELETE FROM {}subjects WHERE id=?";
    }

    protected String getDeleteSubjectTypeNameQuery() {
        return "DELETE FROM {}subjects WHERE type=? AND identifier=?";
    }

    protected String getInsertSubjectTypeNameQuery() {
        return "INSERT INTO {}subjects (type, identifier) VALUES (?, ?)";
    }

    protected String getSelectContextsSegmentQuery() {
        return "SELECT `key`, `value` FROM {}contexts WHERE segment=?";
    }

    protected String getSelectSegmentsSubjectQuery() {
        return "SELECT id, perm_default FROM {}segments WHERE subject=?";
    }

    protected String getSelectPermissionsSegmentQuery() {
        return "SELECT `key`, `value` FROM {}permissions WHERE segment=?";
    }

    protected String getSelectOptionsSegmentQuery() {
        return "SELECT `key`, `value` FROM {}options WHERE segment=?";
    }

    protected String getSelectInheritanceSegmentQuery() {
        return "SELECT * FROM {}inheritance LEFT JOIN ({}subjects) on ({}inheritance.parent={}subjects.id) WHERE segment=?";
    }

    protected String getInsertSegmentQuery() {
        return "INSERT INTO {}segments (subject, perm_default) VALUES (?, ?)";
    }

    protected String getDeleteSegmentIdQuery() {
        return "DELETE FROM {}segments WHERE id=?";
    }

    protected String getSelectSubjectIdentifiersQuery() {
        return "SELECT identifier FROM {}subjects WHERE type=?";
    }

    protected String getSelectSubjectTypesQuery() {
        return "SELECT DISTINCT type FROM {}subjects";
    }

    protected String getDeleteOptionKeyQuery() {
        return "DELETE FROM {}options WHERE segment=? AND `key`=?";
    }

    protected String getDeleteOptionsQuery() {
        return "DELETE FROM {}options WHERE segment=?";
    }

    protected abstract String getInsertOptionUpdatingQuery();

    protected abstract String getInsertPermissionUpdatingQuery();

    protected String getDeletePermissionKeyQuery() {
        return "DELETE FROM {}permissions WHERE segment=? AND `key`=?";
    }

    protected String getDeletePermissionsQuery() {
        return "DELETE FROM {}permissions WHERE segment=?";
    }

    protected String getUpdatePermissionDefaultQuery() {
        return "UPDATE {}segments SET perm_default=? WHERE id=?";
    }

    protected String getInsertInheritanceQuery() {
        return "INSERT INTO {}inheritance (`segment`, `parent`) VALUES (?, ?)";
    }

    protected String getDeleteInheritanceParentQuery() {
        return "DELETE FROM {}inheritance WHERE segment=? AND parent=?";
    }

    protected String getDeleteInheritanceQuery() {
        return "DELETE FROM {}inheritance WHERE segment=?";
    }

    protected String getInsertContextQuery() {
        return "INSERT INTO {}contexts (segment, `key`, `value`) VALUES (?, ?, ?)";
    }

    protected String getDeleteContextQuery() {
        return "DELETE FROM {}contexts WHERE segment=?";
    }

    protected String getSelectContextInheritanceQuery() {
        return "SELECT `child_key`, `child_value`, `parent_key`, `parent_value` FROM {}context_inheritance ORDER BY `child_key`, `child_value`, `id` ASC";
    }

    protected String getInsertContextInheritanceQuery() {
        return "INSERT INTO {}context_inheritance (child_key, child_value, parent_key, parent_value) VALUES (?, ?, ?, ?)";
    }

    protected String getDeleteContextInheritanceQuery() {
        return "DELETE FROM {}context_inheritance WHERE child_key=? AND child_value=?";
    }

    protected String getSelectRankLadderQuery() {
        return "SELECT `{}rank_ladders`.`id`, `subject`, `type`, `identifier` FROM {}rank_ladders LEFT JOIN (`{}subjects`) ON (`{}rank_ladders`.`subject`=`{}subjects`.`id`) WHERE `name`=? ORDER BY `{}rank_ladders`.`id` ASC";
    }

    protected String getTestRankLadderExistsQuery() {
        return "SELECT `id` FROM {}rank_ladders WHERE `name`=? LIMIT 1";
    }

    protected String getInsertRankLadderQuery() {
        return "INSERT INTO {}rank_ladders (`name`, `subject`) VALUES (?, ?)";
    }

    protected String getDeleteRankLadderQuery() {
        return "DELETE FROM {}rank_ladders WHERE `name`=?";
    }

    protected String getSelectAllRankLadderNamesQuery() {
        return "SELECT DISTINCT `name` FROM {}rank_ladders";
    }

    protected String getSelectAllSubjectsQuery() {
        return "SELECT `id`, `type`, `identifier` FROM {}subjects";
    }

    public String getRenameTableQuery() {
        return "ALTER TABLE ? RENAME ?";
    }

    public PreparedStatement prepareStatement(String query) throws SQLException {
        return conn.prepareStatement(this.ds.insertPrefix(query));
    }

    protected PreparedStatement prepareStatement(String query, int params) throws SQLException {
        return conn.prepareStatement(this.ds.insertPrefix(query), params);
    }

    protected <T> T executeInTransaction(ThrowingSupplier<T, SQLException> func) throws SQLException {
        transactionLevel++;
        conn.setAutoCommit(false);
        try {
            T ret = func.supply();
            if (--transactionLevel <= 0) {
                conn.commit();
            }
            return ret;
        } finally {
            if (transactionLevel <= 0) {
                conn.setAutoCommit(true);
            }
        }
    }

    // -- Operations

    public LegacyDao legacy() {
        return LegacyDao.INSTANCE;
    }

    public Optional<String> getGlobalParameter(String key) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectGlobalParameterQuery())) {
            stmt.setString(1, key);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return Optional.of(rs.getString(1));
            } else {
                return Optional.empty();
            }
        }

    }

    public void setGlobalParameter(String key, String value) throws SQLException {
        if (value == null) {
            try (PreparedStatement stmt = prepareStatement(getDeleteGlobalParameterQuery())) {
                stmt.setString(1, key);
                stmt.executeUpdate();
            }
        } else {
            try (PreparedStatement stmt = prepareStatement(getInsertGlobalParameterQueryUpdating())) {
                stmt.setString(1, key);
                stmt.setString(2, value);
                stmt.executeUpdate();
            }
        }
    }

    public Optional<SubjectRef> getSubjectRef(int id) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getGetSubjectRefIdQuery())) {
            stmt.setInt(1, id);
            ResultSet res = stmt.executeQuery();

            if (!res.next()) {
                return Optional.empty();
            }
            return Optional.of(new SubjectRef(id, res.getString(1), res.getString(2)));
        }
    }

    public Optional<SubjectRef> getSubjectRef(String type, String name) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getGetSubjectRefTypeNameQuery())) {
            stmt.setString(1, type);
            stmt.setString(2, name);
            ResultSet res = stmt.executeQuery();

            if (!res.next()) {
                return Optional.empty();
            }
            return Optional.of(new SubjectRef(res.getInt(1), type, name));
        }
    }

    public boolean removeSubject(SubjectRef ref) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeleteSubjectIdQuery())) {
            stmt.setInt(1, ref.getId());
            return stmt.executeUpdate() > 0;
        }
    }

    public boolean removeSubject(String type, String name) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeleteSubjectTypeNameQuery())) {
            stmt.setString(1, type);
            stmt.setString(2, name);
            return stmt.executeUpdate() > 0;
        }
    }

    public SubjectRef getOrCreateSubjectRef(String type, String name) throws SQLException {
        final SubjectRef ret = SubjectRef.unresolved(type, name);
        allocateSubjectRef(ret);
        return ret;
    }

    public void allocateSubjectRef(SubjectRef ref) throws SQLException {
        executeInTransaction(() -> {
            try (PreparedStatement stmt = prepareStatement(getGetSubjectRefTypeNameQuery())) {
                stmt.setString(1, ref.getType());
                stmt.setString(2, ref.getIdentifier());
                ResultSet res = stmt.executeQuery();
                if (res.next()) {
                    ref.setId(res.getInt(1));
                } else {
                    try (PreparedStatement addStatement = prepareStatement(getInsertSubjectTypeNameQuery(),
                            Statement.RETURN_GENERATED_KEYS)) {
                        addStatement.setString(1, ref.getType());
                        addStatement.setString(2, ref.getIdentifier());
                        addStatement.executeUpdate();
                        res = addStatement.getGeneratedKeys();
                        res.next();
                        ref.setId(res.getInt(1));
                    }
                }
            }
            return ref;
        });
    }

    public int getIdAllocating(SubjectRef ref) throws SQLException {
        if (ref.isUnallocated()) {
            allocateSubjectRef(ref);
        }
        return ref.getId();
    }

    private Set<Entry<String, String>> getSegmentContexts(int segmentId) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectContextsSegmentQuery())) {
            stmt.setInt(1, segmentId);
            ImmutableSet.Builder<Entry<String, String>> res = ImmutableSet.builder();

            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                res.add(Maps.immutableEntry(rs.getString(1), rs.getString(2)));
            }
            return res.build();
        }
    }

    public List<Segment> getSegments(SubjectRef ref) throws SQLException {
        ImmutableList.Builder<Segment> result = ImmutableList.builder();
        try (PreparedStatement stmt = prepareStatement(getSelectSegmentsSubjectQuery())) {
            stmt.setInt(1, getIdAllocating(ref));
            ResultSet rs = stmt.executeQuery();

            while (rs.next()) {
                final int id = rs.getInt(1);
                Number permDef = (Number) rs.getObject(2);
                Set<Entry<String, String>> contexts = getSegmentContexts(id);

                ImmutableMap.Builder<String, Integer> permValues = ImmutableMap.builder();
                ImmutableMap.Builder<String, String> optionValues = ImmutableMap.builder();
                ImmutableList.Builder<SubjectRef> inheritanceValues = ImmutableList.builder();

                try (PreparedStatement permStmt = prepareStatement(getSelectPermissionsSegmentQuery())) {
                    permStmt.setInt(1, id);

                    ResultSet segmentRs = permStmt.executeQuery();
                    while (segmentRs.next()) {
                        permValues.put(segmentRs.getString(1), segmentRs.getInt(2));
                    }
                }

                try (PreparedStatement optStmt = prepareStatement(getSelectOptionsSegmentQuery())) {
                    optStmt.setInt(1, id);

                    ResultSet segmentRs = optStmt.executeQuery();
                    while (segmentRs.next()) {
                        optionValues.put(segmentRs.getString(1), segmentRs.getString(2));
                    }
                }

                try (PreparedStatement inheritStmt = prepareStatement(getSelectInheritanceSegmentQuery())) {
                    inheritStmt.setInt(1, id);

                    ResultSet segmentRs = inheritStmt.executeQuery();
                    while (segmentRs.next()) {
                        inheritanceValues.add(new SubjectRef(segmentRs.getInt(3), segmentRs.getString(4),
                                segmentRs.getString(5)));
                    }
                }

                result.add(new Segment(id, contexts, permValues.build(), optionValues.build(),
                        inheritanceValues.build(), permDef == null ? null : permDef.intValue(), null));

            }
        }
        return result.build();
    }

    public Segment addSegment(SubjectRef ref) throws SQLException { // TODO: Is this method useful?
        Segment segment = Segment.unallocated();
        allocateSegment(ref, segment);
        return segment;
    }

    public void updateFullSegment(SubjectRef ref, Segment segment) throws SQLException {
        executeInTransaction(() -> {
            allocateSegment(ref, segment);
            setContexts(segment, segment.getContexts());
            setOptions(segment, segment.getOptions());
            setParents(segment, segment.getParents());
            setPermissions(segment, segment.getPermissions());
            setDefaultValue(segment, segment.getPermissionDefault());
            return null;
        });
    }

    public void setContexts(Segment seg, Set<Entry<String, String>> contexts) throws SQLException {
        // Update contexts
        executeInTransaction(() -> {
            try (PreparedStatement delete = prepareStatement(getDeleteContextQuery());
                    PreparedStatement insert = prepareStatement(getInsertContextQuery())) {
                delete.setInt(1, seg.getId());
                delete.executeUpdate();

                insert.setInt(1, seg.getId());

                for (Map.Entry<String, String> context : contexts) {
                    insert.setString(2, context.getKey());
                    insert.setString(3, context.getValue());
                    insert.addBatch();
                }
                insert.executeBatch();
            }
            return null;
        });

    }

    public void allocateSegment(SubjectRef subject, Segment val) throws SQLException {
        if (!val.isUnallocated()) {
            return;
        }

        try (PreparedStatement stmt = prepareStatement(getInsertSegmentQuery(), Statement.RETURN_GENERATED_KEYS)) {
            stmt.setInt(1, getIdAllocating(subject));
            if (val.getPermissionDefault() == null) {
                stmt.setNull(2, Types.INTEGER);
            } else {
                stmt.setInt(2, val.getPermissionDefault());
            }

            stmt.executeUpdate();
            ResultSet res = stmt.getGeneratedKeys();
            res.next();
            val.setId(res.getInt(1));
        }
        setContexts(val, val.getContexts());
    }

    public boolean removeSegment(Segment segment) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeleteSegmentIdQuery())) {
            stmt.setInt(1, segment.getId());
            return stmt.executeUpdate() > 0;
        }
    }

    public Set<String> getAllIdentifiers(String type) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectSubjectIdentifiersQuery())) {
            stmt.setString(1, type);

            ResultSet rs = stmt.executeQuery();
            ImmutableSet.Builder<String> ret = ImmutableSet.builder();

            while (rs.next()) {
                ret.add(rs.getString(1));
            }

            return ret.build();
        }
    }

    public Set<String> getRegisteredTypes() throws SQLException {
        try (ResultSet rs = prepareStatement(getSelectSubjectTypesQuery()).executeQuery()) {
            ImmutableSet.Builder<String> ret = ImmutableSet.builder();

            while (rs.next()) {
                ret.add(rs.getString(1));
            }

            return ret.build();
        }
    }

    public void initializeTables() throws SQLException {
        if (hasTable("permissions")) {
            return;
        }
        String database = conn.getMetaData().getDatabaseProductName().toLowerCase();
        try (InputStream res = SqlDao.class.getResourceAsStream("deploy/" + database + ".sql")) {
            if (res == null) {
                throw new SQLException("No initial schema available for " + database + " databases!");
            }
            try (BufferedReader read = new BufferedReader(new InputStreamReader(res, StandardCharsets.UTF_8))) {
                executeStream(read);
            }
        } catch (IOException e) {
            throw new SQLException(e);
        }
    }

    void executeStream(BufferedReader reader) throws SQLException, IOException {
        try (Statement stmt = conn.createStatement()) {
            StringBuilder currentQuery = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("--")) {
                    continue;
                }

                currentQuery.append(line);
                if (line.endsWith(";")) {
                    currentQuery.deleteCharAt(currentQuery.length() - 1);
                    String queryLine = currentQuery.toString().trim();
                    currentQuery = new StringBuilder();
                    if (!queryLine.isEmpty()) {
                        stmt.addBatch(ds.insertPrefix(queryLine));
                    }
                }
            }
            stmt.executeBatch();
        }

    }

    private boolean hasTable(String table) throws SQLException {
        return conn.getMetaData().getTables(null, null, this.ds.getTableName(table).toUpperCase(), null).next(); // Upper-case for H2
    }

    public void clearOption(Segment segment, String option) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeleteOptionKeyQuery())) {
            stmt.setInt(1, segment.getId());
            stmt.setString(2, option);
            stmt.executeUpdate();
        }
    }

    public void setOptions(Segment seg, Map<String, String> options) throws SQLException {
        executeInTransaction(() -> {
            try (PreparedStatement del = prepareStatement(getDeleteOptionsQuery());
                    PreparedStatement ins = prepareStatement(getInsertOptionUpdatingQuery())) {
                del.setInt(1, seg.getId());
                del.executeUpdate();

                if (options != null) {
                    ins.setInt(1, seg.getId());
                    for (Map.Entry<String, String> ent : options.entrySet()) {
                        ins.setString(2, ent.getKey());
                        ins.setString(3, ent.getValue());
                        ins.addBatch();
                    }
                    ins.executeBatch();
                }
            }
            return null;
        });
    }

    public void setOption(Segment segment, String key, String value) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getInsertOptionUpdatingQuery())) {
            stmt.setInt(1, segment.getId());
            stmt.setString(2, key);
            stmt.setString(3, value);
            stmt.executeUpdate();
        }
    }

    public void setPermission(Segment segment, String permission, int value) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getInsertPermissionUpdatingQuery())) {
            stmt.setInt(1, segment.getId());
            stmt.setString(2, permission);
            stmt.setInt(3, value);
            stmt.executeUpdate();
        }

    }

    public void clearPermission(Segment segment, String permission) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeletePermissionKeyQuery())) {
            stmt.setInt(1, segment.getId());
            stmt.setString(2, permission);
            stmt.executeUpdate();
        }
    }

    public void setPermissions(Segment segment, Map<String, Integer> permissions) throws SQLException {
        executeInTransaction(() -> {
            try (PreparedStatement del = prepareStatement(getDeletePermissionsQuery());
                    PreparedStatement ins = prepareStatement(getInsertPermissionUpdatingQuery())) {
                del.setInt(1, segment.getId());
                del.executeUpdate();

                if (permissions != null) {
                    ins.setInt(1, segment.getId());
                    for (Map.Entry<String, Integer> ent : permissions.entrySet()) {
                        ins.setString(2, ent.getKey());
                        ins.setInt(3, ent.getValue());
                        ins.addBatch();
                    }
                    ins.executeBatch();
                }
            }
            return null;
        });
    }

    public void setDefaultValue(Segment segment, Integer permissionDefault) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getUpdatePermissionDefaultQuery())) {
            if (permissionDefault == null) {
                stmt.setNull(1, Types.INTEGER);
            } else {
                stmt.setInt(1, permissionDefault);
            }
            stmt.setInt(2, segment.getId());
            stmt.executeUpdate();
        }
    }

    public void addParent(Segment seg, SubjectRef parent) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getInsertInheritanceQuery())) {
            stmt.setInt(1, seg.getId());
            stmt.setInt(2, getIdAllocating(parent));
            stmt.executeUpdate();
        }
    }

    public void removeParent(Segment segment, SubjectRef parent) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getDeleteInheritanceParentQuery())) {
            stmt.setInt(1, segment.getId());
            stmt.setInt(2, getIdAllocating(parent));
            stmt.executeUpdate();
        }
    }

    public void setParents(Segment segment, List<SubjectRef> parents) throws SQLException {
        executeInTransaction(() -> {
            try (PreparedStatement del = prepareStatement(getDeleteInheritanceQuery());
                    PreparedStatement ins = prepareStatement(getInsertInheritanceQuery())) {
                del.setInt(1, segment.getId());
                del.executeUpdate();

                if (parents != null) {
                    ins.setInt(1, segment.getId());
                    for (SubjectRef ent : parents) {
                        ins.setInt(2, getIdAllocating(ent));
                        ins.addBatch();
                    }
                    ins.executeBatch();
                }
            }
            return null;
        });
    }

    public SqlContextInheritance getContextInheritance() throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectContextInheritanceQuery())) {
            ImmutableMap.Builder<Entry<String, String>, List<Entry<String, String>>> ret = ImmutableMap.builder();
            Entry<String, String> current = null;
            ImmutableList.Builder<Entry<String, String>> builder = null;
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                final String childKey = rs.getString(1), childValue = rs.getString(2), parentKey = rs.getString(3),
                        parentValue = rs.getString(4);
                if (current == null || !childKey.equals(current.getKey())
                        || !childValue.equals(current.getValue())) {
                    if (current != null && builder != null) {
                        ret.put(current, builder.build());
                    }
                    current = Maps.immutableEntry(childKey, childValue);
                    builder = ImmutableList.builder();
                }
                builder.add(Maps.immutableEntry(parentKey, parentValue));
            }

            if (current != null) {
                ret.put(current, builder.build());
            }

            return new SqlContextInheritance(ret.build(), null);
        }

    }

    public void setContextInheritance(Entry<String, String> child, List<Entry<String, String>> parents)
            throws SQLException {
        executeInTransaction(() -> {
            try (PreparedStatement delete = prepareStatement(getDeleteContextInheritanceQuery());
                    PreparedStatement insert = prepareStatement(getInsertContextInheritanceQuery())) {
                delete.setString(1, child.getKey());
                delete.setString(2, child.getValue());
                delete.executeUpdate();

                if (parents != null && parents.size() > 0) {
                    insert.setString(1, child.getKey());
                    insert.setString(2, child.getValue());
                    for (Entry<String, String> parent : parents) {
                        insert.setString(3, parent.getKey());
                        insert.setString(4, parent.getValue());
                        insert.addBatch();
                    }
                    insert.executeBatch();
                }
            }
            return null;
        });
    }

    public RankLadder getRankLadder(String name) throws SQLException {
        ImmutableList.Builder<SubjectRef> elements = ImmutableList.builder();
        try (PreparedStatement stmt = prepareStatement(getSelectRankLadderQuery())) {
            stmt.setString(1, name);
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                elements.add(new SubjectRef(rs.getInt(2), rs.getString(3), rs.getString(4)));
            }
        }
        return new SqlRankLadder(name, elements.build());
    }

    public boolean hasEntriesForRankLadder(String name) throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getTestRankLadderExistsQuery())) {
            stmt.setString(1, name);
            return stmt.executeQuery().next();
        }
    }

    public void setRankLadder(String name, RankLadder ladder) throws SQLException {
        executeInTransaction(() -> {
            List<SubjectRef> ranks;
            if (ladder == null) {
                ranks = ImmutableList.of();
            } else if (ladder instanceof SqlRankLadder) {
                ranks = ((SqlRankLadder) ladder).getRanks();
            } else {
                ranks = Lists.transform(ladder.getRanks(), rank -> rank instanceof SubjectRef ? (SubjectRef) rank
                        : SubjectRef.unresolved(rank.getKey(), rank.getValue()));
            }

            try (PreparedStatement delete = prepareStatement(getDeleteRankLadderQuery());
                    PreparedStatement insert = prepareStatement(getInsertRankLadderQuery())) {
                delete.setString(1, name);
                delete.executeUpdate();

                if (ladder != null) {
                    insert.setString(1, name);
                    for (SubjectRef ref : ranks) {
                        insert.setInt(2, getIdAllocating(ref));
                        insert.addBatch();
                    }
                    insert.executeBatch();
                }
            }
            return null;
        });
    }

    public Iterable<String> getAllRankLadderNames() throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectAllRankLadderNamesQuery())) {
            ImmutableSet.Builder<String> ret = ImmutableSet.builder();
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                ret.add(rs.getString(1));
            }
            return ret.build();
        }
    }

    public Iterable<SubjectRef> getAllSubjectRefs() throws SQLException {
        try (PreparedStatement stmt = prepareStatement(getSelectAllSubjectsQuery())) {
            ImmutableSet.Builder<SubjectRef> ret = ImmutableSet.builder();
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                ret.add(new SubjectRef(rs.getInt(1), rs.getString(2), rs.getString(3)));
            }
            return ret.build();
        }
    }

    @Override
    public void close() throws SQLException {
        if (this.holdOpen <= 0) {
            this.conn.close();
        }
    }

    public void renameTable(String oldName, String newName) throws SQLException {
        final String expandedOld = ds.getTableName(oldName);
        final String expandedNew = ds.getTableName(newName);
        try (PreparedStatement stmt = prepareStatement(getRenameTableQuery())) {
            stmt.setString(1, expandedOld);
            stmt.setString(2, expandedNew);
            stmt.executeUpdate();
        }
    }

    public void deleteTable(String table) throws SQLException {
        try (PreparedStatement stmt = prepareStatement("DROP TABLE " + ds.getTableName(table))) {
            stmt.executeUpdate();
        }
    }

    /**
     * Get the schema version. Has to include backwards compatibility to correctly handle pre-2.x schema updates.
     *
     * @return The schema version,
     * @throws SQLException
     */
    public int getSchemaVersion() throws SQLException {
        if (hasTable("global")) { // Current
            return getGlobalParameter(SqlConstants.OPTION_SCHEMA_VERSION).map(Integer::valueOf)
                    .orElse(SqlConstants.VERSION_PRE_VERSIONING);
        } else if (legacy().hasTable(this, "permissions")) { // Legacy option
            String ret = legacy().getOption(this, "system", LegacyMigration.Type.WORLD, null, "schema-version");
            return ret == null ? SqlConstants.VERSION_PRE_VERSIONING : Integer.valueOf(ret);
        } else {
            return SqlConstants.VERSION_NOT_INITIALIZED;
        }
    }

    public void setSchemaVersion(int version) throws SQLException {
        setGlobalParameter(SqlConstants.OPTION_SCHEMA_VERSION, Integer.toString(version));
    }

    public SqlDataStore getDataStore() {
        return ds;
    }

    public Connection getConnection() {
        return conn;
    }
}