001/* 002 * PermissionsEx 003 * Copyright (C) zml and PermissionsEx contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ca.stellardrift.permissionsex.datastore.sql; 018 019import ca.stellardrift.permissionsex.PermissionsEngine; 020import ca.stellardrift.permissionsex.datastore.sql.dao.LegacyMigration; 021import ca.stellardrift.permissionsex.datastore.sql.dao.SchemaMigration; 022import ca.stellardrift.permissionsex.impl.util.PCollections; 023import ca.stellardrift.permissionsex.legacy.LegacyConversions; 024import ca.stellardrift.permissionsex.subject.SubjectRef; 025import ca.stellardrift.permissionsex.context.ContextValue; 026import org.checkerframework.checker.nullness.qual.Nullable; 027import org.pcollections.PVector; 028 029import java.sql.PreparedStatement; 030import java.sql.ResultSet; 031import java.util.ArrayDeque; 032import java.util.ArrayList; 033import java.util.Deque; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Objects; 038 039import static org.spongepowered.configurate.util.UnmodifiableCollections.immutableMapEntry; 040 041/** 042 * Schema migrations for the SQL database 043 */ 044public class SchemaMigrations { 045 public static final int VERSION_LATEST = 3; 046 047 public static List<SchemaMigration> getMigrations() { 048 List<SchemaMigration> migrations = new ArrayList<>(); 049 migrations.add(0, SchemaMigrations.initialToZero()); 050 migrations.add(1, SchemaMigrations.zeroToOne()); 051 migrations.add(2, SchemaMigrations.oneToTwo()); 052 migrations.add(VERSION_LATEST, SchemaMigrations.twoToThree()); 053 return migrations; 054 } 055 056 // Pre-2.x only needs to support MySQL because tbh nobody uses SQLite 057 public static SchemaMigration twoToThree() { 058 // The big one 059 return dao -> { 060 dao.legacy().renameTable(dao, "permissions", "permissions_old"); 061 dao.legacy().renameTable(dao, "permissions_entity", "permissions_entity_old"); 062 dao.legacy().renameTable(dao, "permissions_inheritance", "permissions_inheritance_old"); 063 dao.initializeTables(); 064 065 // Transfer world inheritance 066 try (PreparedStatement stmt = dao.prepareStatement("SELECT id, child, parent FROM {}permissions_inheritance_old WHERE type=2 ORDER BY child, parent, id ASC")) { 067 ResultSet rs = stmt.executeQuery(); 068 try (PreparedStatement insert = dao.prepareStatement(dao.getInsertContextInheritanceQuery())) { 069 insert.setString(1, "world"); 070 insert.setString(3, "world"); 071 while (rs.next()) { 072 insert.setString(2, rs.getString(2)); 073 insert.setString(4, rs.getString(3)); 074 insert.addBatch(); 075 } 076 insert.executeBatch(); 077 } 078 } 079 080 Map<String, List<SqlSubjectRef<?>>> defaultSubjects = new HashMap<>(); 081 Map<String, List<Map.Entry<SqlSubjectRef<?>, Integer>>> tempRankLadders = new HashMap<>(); 082 083 try (PreparedStatement select = dao.prepareStatement("SELECT type, name FROM {}permissions_entity_old")) { 084 ResultSet rs = select.executeQuery(); 085 while (rs.next()) { 086 final SqlSubjectRef<?> ref = dao.getOrCreateSubjectRef(LegacyMigration.Type.values()[rs.getInt(1)].name().toLowerCase(), rs.getString(2)); 087 @Nullable SqlSegment currentSeg = null; 088 @Nullable String currentWorld = null; 089 Map<String, SqlSegment> worldSegments = new HashMap<>(); 090 try (PreparedStatement selectPermissionsOptions = dao.prepareStatement("SELECT id, permission, world, value FROM {}permissions_old WHERE type=? AND name=? ORDER BY world, id DESC")) { 091 selectPermissionsOptions.setInt(1, rs.getInt(1)); 092 selectPermissionsOptions.setString(2, rs.getString(2)); 093 094 ResultSet perms = selectPermissionsOptions.executeQuery(); 095 Map<String, Integer> newPerms = new HashMap<>(); 096 Map<String, String> options = new HashMap<>(); 097 @Nullable String rank = null; 098 @Nullable String rankLadder = null; 099 int defaultVal = 0; 100 while (perms.next()) { 101 @Nullable String worldChecked = perms.getString(3); 102 if (worldChecked != null && worldChecked.isEmpty()) { 103 worldChecked = null; 104 } 105 if (currentSeg == null || !Objects.equals(worldChecked, currentWorld)) { 106 if (currentSeg != null) { 107 if (!options.isEmpty()) { 108 dao.setOptions(currentSeg, options); 109 options.clear(); 110 } 111 if (!newPerms.isEmpty()) { 112 dao.setPermissions(currentSeg, newPerms); 113 newPerms.clear(); 114 } 115 if (defaultVal != 0) { 116 dao.setDefaultValue(currentSeg, defaultVal); 117 defaultVal = 0; 118 } 119 } 120 currentWorld = worldChecked; 121 currentSeg = SqlSegment.unallocated(currentWorld == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", currentWorld))); 122 dao.allocateSegment(ref, currentSeg); 123 worldSegments.put(currentWorld, currentSeg); 124 } 125 String key = perms.getString(2); 126 final String value = perms.getString(4); 127 if (value == null || value.isEmpty()) { 128 // permission 129 int val = key.startsWith("-") ? -1 : 1; 130 if (val == -1) { 131 key = key.substring(1); 132 } 133 if (key.equals("*")) { 134 defaultVal = val; 135 continue; 136 } 137 key = LegacyConversions.convertLegacyPermission(key); 138 newPerms.put(key, val); 139 } else { 140 if (currentWorld == null) { 141 boolean rankEq = key.equals("rank"), rankLadderEq = !rankEq && key.equals("rank-ladder"); 142 if (rankEq || rankLadderEq) { 143 if (rankEq) { 144 rank = value; 145 } else { // then it's the rank ladder 146 rankLadder = value; 147 } 148 if (rank != null && rankLadder != null) { 149 List<Map.Entry<SqlSubjectRef<?>, Integer>> ladder = tempRankLadders.computeIfAbsent(rankLadder, ign -> new ArrayList<>()); 150 try { 151 ladder.add(immutableMapEntry(ref, Integer.parseInt(rank))); 152 } catch (IllegalArgumentException ignore) { 153 // non-integer rank TODO maybe warn 154 } 155 rankLadder = null; 156 rank = null; 157 } 158 continue; 159 } 160 } 161 if (key.equals("default") && value.equalsIgnoreCase("true")) { 162 defaultSubjects.computeIfAbsent(currentWorld, ign -> new ArrayList<>()).add(ref); 163 continue; 164 } 165 options.put(key, value); 166 } 167 } 168 169 if (currentSeg != null) { 170 if (!options.isEmpty()) { 171 dao.setOptions(currentSeg, options); 172 } 173 if (!newPerms.isEmpty()) { 174 dao.setPermissions(currentSeg, newPerms); 175 } 176 if (defaultVal != 0) { 177 dao.setDefaultValue(currentSeg, defaultVal); 178 } 179 if (rank != null) { 180 List<Map.Entry<SqlSubjectRef<?>, Integer>> ladder = tempRankLadders.computeIfAbsent("default", ign -> new ArrayList<>()); 181 try { 182 ladder.add(immutableMapEntry(ref, Integer.parseInt(rank))); 183 } catch (IllegalArgumentException ex) { 184 // non-integer rank TODO maybe warn 185 } 186 187 } 188 } 189 } 190 191 for (Map.Entry<String, List<Map.Entry<SqlSubjectRef<?>, Integer>>> ent : tempRankLadders.entrySet()) { 192 PVector<SubjectRef<?>> ladder = ent.getValue().stream() 193 .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) 194 .map(Map.Entry::getKey) 195 .collect(PCollections.toPVector()); 196 dao.setRankLadder(ent.getKey(), new SqlRankLadder(ent.getKey(), ladder)); 197 198 } 199 200 if (!defaultSubjects.isEmpty()) { 201 final SqlSubjectRef<?> defaultSubj = dao.getOrCreateSubjectRef(dao.getDataStore().ctx().engine().fallbacks().type().name(), LegacyConversions.SUBJECTS_USER); 202 final List<SqlSegment> segments = new ArrayList<>(dao.getSegments(defaultSubj)); 203 for (Map.Entry<String, List<SqlSubjectRef<?>>> ent : defaultSubjects.entrySet()) { 204 SqlSegment seg = null; 205 if (!segments.isEmpty()) { 206 for (SqlSegment segment : segments) { 207 if (ent.getKey() == null && segment.contexts().isEmpty()) { 208 seg = segment; 209 break; 210 } else if (segment.contexts().size() == 1) { 211 ContextValue<?> ctx = segment.contexts().iterator().next(); 212 if (ctx.key().equals("world") && ctx.rawValue().equals(ent.getKey())) { 213 seg = segment; 214 break; 215 } 216 } 217 } 218 } 219 if (seg == null) { 220 seg = SqlSegment.unallocated(ent.getKey() == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", ent.getKey()))); 221 dao.allocateSegment(defaultSubj, seg); 222 segments.add(seg); 223 } 224 dao.setParents(seg, ent.getValue()); 225 } 226 } 227 228 try (PreparedStatement selectInheritance = dao.prepareStatement(dao.legacy().getSelectParentsQuery())) { 229 selectInheritance.setString(1, rs.getString(2)); 230 selectInheritance.setInt(2, rs.getInt(1)); 231 232 ResultSet inheritance = selectInheritance.executeQuery(); 233 Deque<SqlSubjectRef<?>> newInheritance = new ArrayDeque<>(); 234 while (inheritance.next()) { 235 if (currentSeg == null || !Objects.equals(inheritance.getString(3), currentWorld)) { 236 if (currentSeg != null && !newInheritance.isEmpty()) { 237 dao.setParents(currentSeg, newInheritance); 238 newInheritance.clear(); 239 } 240 currentWorld = inheritance.getString(3); 241 currentSeg = worldSegments.get(currentWorld); 242 if (currentSeg == null) { 243 currentSeg = SqlSegment.unallocated(currentWorld == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", currentWorld))); 244 dao.allocateSegment(ref, currentSeg); 245 worldSegments.put(currentWorld, currentSeg); 246 } 247 } 248 newInheritance.add(dao.getOrCreateSubjectRef(LegacyConversions.SUBJECTS_GROUP, inheritance.getString(2))); 249 } 250 if (currentSeg != null && !newInheritance.isEmpty()) { 251 dao.setParents(currentSeg, newInheritance); 252 newInheritance.clear(); 253 } 254 } 255 256 } 257 } 258 259 dao.deleteTable("permissions_old"); 260 dao.deleteTable("permissions_entity_old"); 261 dao.deleteTable("permissions_inheritance_old"); 262 }; 263 } 264 265 public static SchemaMigration oneToTwo() { 266 return dao -> { 267 // Change encoding for all columns to utf8mb4 268 // Change collation for all columns to utf8mb4_general_ci 269 dao.legacy().prepareStatement(dao, "ALTER TABLE `{permissions}` DROP KEY `unique`, MODIFY COLUMN `permission` TEXT NOT NULL").execute(); 270 }; 271 } 272 273 public static SchemaMigration zeroToOne() { 274 return dao -> { 275 PreparedStatement updateStmt = dao.prepareStatement(dao.legacy().getInsertOptionQuery()); 276 ResultSet res = dao.legacy().prepareStatement(dao, "SELECT `name`, `type` FROM `{permissions_entity}` WHERE `default`='1'").executeQuery(); 277 while (res.next()) { 278 updateStmt.setString(1, res.getString(1)); 279 updateStmt.setInt(2, res.getInt(2)); 280 updateStmt.setString(3, "default"); 281 updateStmt.setString(4, ""); 282 updateStmt.setString(5, "true"); 283 updateStmt.addBatch(); 284 } 285 updateStmt.executeBatch(); 286 287 // Update tables 288 dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP COLUMN `default`").execute(); 289 }; 290 } 291 292 public static SchemaMigration initialToZero() { 293 return (LegacyMigration) dao -> { 294 // TODO: Table modifications not supported in SQLite 295 // Prefix/sufix -> options 296 PreparedStatement updateStmt = dao.legacy().prepareStatement(dao, dao.legacy().getInsertOptionQuery()); 297 ResultSet res = dao.prepareStatement("SELECT `name`, `type`, `prefix`, `suffix` FROM `{permissions_entity}` WHERE LENGTH(`prefix`)>0 OR LENGTH(`suffix`)>0").executeQuery(); 298 while (res.next()) { 299 String prefix = res.getString("prefix"); 300 if (!prefix.isEmpty() && !prefix.equals("null")) { 301 updateStmt.setString(1, res.getString(1)); 302 updateStmt.setInt(2, res.getInt(2)); 303 updateStmt.setString(3, "prefix"); 304 updateStmt.setString(4, ""); 305 updateStmt.setString(5, prefix); 306 updateStmt.addBatch(); 307 } 308 String suffix = res.getString("suffix"); 309 if (!suffix.isEmpty() && !suffix.equals("null")) { 310 updateStmt.setString(1, res.getString(1)); 311 updateStmt.setInt(2, res.getInt(2)); 312 updateStmt.setString(3, "suffix"); 313 updateStmt.setString(4, ""); 314 updateStmt.setString(5, suffix); 315 updateStmt.addBatch(); 316 } 317 } 318 updateStmt.executeBatch(); 319 320 // Data type corrections 321 322 // Update tables 323 dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP KEY `name`").execute(); 324 dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP COLUMN `prefix`, DROP COLUMN `suffix`").execute(); 325 dao.prepareStatement("ALTER TABLE `{permissions_entity}` ADD CONSTRAINT UNIQUE KEY `name` (`name`, `type`)").execute(); 326 327 dao.prepareStatement("ALTER TABLE `{permissions}` DROP KEY `unique`").execute(); 328 dao.prepareStatement("ALTER TABLE `{permissions}` ADD CONSTRAINT UNIQUE `unique` (`name`,`permission`,`world`,`type`)").execute(); 329 }; 330 331 } 332}