Skip to content

Commit 3bb09e4

Browse files
committed
Update SQL.java
1 parent 888b50c commit 3bb09e4

1 file changed

Lines changed: 124 additions & 84 deletions

File tree

  • src/main/java/io/github/intisy/utils/database

src/main/java/io/github/intisy/utils/database/SQL.java

Lines changed: 124 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package io.github.intisy.utils.database;
2+
23
import io.github.intisy.simple.logger.EmptyLogger;
34
import io.github.intisy.simple.logger.SimpleLogger;
45

56
import java.io.File;
67
import java.sql.*;
78
import java.util.*;
89
import java.util.regex.Pattern;
10+
911
@SuppressWarnings({"unused", "SqlNoDataSourceInspection", "SqlSourceToSinkFlow"})
1012
public class SQL {
13+
1114
private static final Pattern VALID_IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
1215
private static final int MAX_IDENTIFIER_LENGTH = 64;
1316

@@ -189,8 +192,7 @@ public void close() {
189192
if (!connection.isClosed()) {
190193
connection.setAutoCommit(true);
191194
}
192-
} catch (SQLException ignored) {
193-
}
195+
} catch (SQLException ignored) {}
194196
}
195197
}
196198
if (!connection.isClosed()) {
@@ -229,8 +231,8 @@ public void createTable(String tableName, List<String> columnDefs, List<String>
229231
if (columnDefs == null || columnDefs.isEmpty()) {
230232
throw new IllegalArgumentException("At least one column definition is required.");
231233
}
232-
for (String colDef : columnDefs) {
233-
if (colDef == null || colDef.trim().isEmpty()) {
234+
for(String colDef : columnDefs) {
235+
if(colDef == null || colDef.trim().isEmpty()) {
234236
throw new IllegalArgumentException("Column definition cannot be null or empty.");
235237
}
236238
}
@@ -241,8 +243,8 @@ public void createTable(String tableName, List<String> columnDefs, List<String>
241243
sql.append(String.join(", ", columnDefs));
242244

243245
if (constraints != null) {
244-
for (String constraint : constraints) {
245-
if (constraint != null && !constraint.trim().isEmpty()) {
246+
for(String constraint : constraints) {
247+
if(constraint != null && !constraint.trim().isEmpty()) {
246248
sql.append(", ").append(constraint);
247249
} else {
248250
throw new IllegalArgumentException("Table constraint cannot be null or empty.");
@@ -656,7 +658,7 @@ public List<Map<String, Object>> selectData(String tableName, List<String> colum
656658
selectColsString = "*";
657659
} else {
658660
List<String> quotedCols = new ArrayList<>();
659-
for (String col : columnsToSelect) {
661+
for(String col : columnsToSelect) {
660662
validateIdentifier(col);
661663
quotedCols.add(quoteIdentifier(col));
662664
}
@@ -728,9 +730,9 @@ public <T> List<T> selectSingleColumn(String tableName, String columnToSelect, M
728730
List<Map<String, Object>> rawResults = selectData(tableName, Collections.singletonList(columnToSelect), whereClause);
729731
List<T> results = new ArrayList<>();
730732

731-
for (Map<String, Object> row : rawResults) {
733+
for(Map<String, Object> row : rawResults) {
732734
Object value = row.get(columnToSelect);
733-
if (value == null) {
735+
if(value == null) {
734736
results.add(null);
735737
} else if (expectedType.isInstance(value)) {
736738
results.add(expectedType.cast(value));
@@ -747,16 +749,17 @@ public <T> List<T> selectSingleColumn(String tableName, String columnToSelect, M
747749
} else if (expectedType == Float.class && value instanceof Number) {
748750
results.add(expectedType.cast(((Number) value).floatValue()));
749751
} else if (expectedType == Boolean.class) {
750-
if (value instanceof Boolean) {
752+
if(value instanceof Boolean) {
751753
results.add(expectedType.cast(value));
752-
} else if (value instanceof Number) {
753-
results.add(expectedType.cast(((Number) value).intValue() != 0));
754+
} else if(value instanceof Number) {
755+
results.add(expectedType.cast(((Number)value).intValue() != 0));
754756
} else if (value instanceof String) {
755-
results.add(expectedType.cast(Boolean.parseBoolean((String) value)));
757+
results.add(expectedType.cast(Boolean.parseBoolean((String)value)));
756758
} else {
757759
throw new ClassCastException("Cannot reliably cast " + value.getClass().getName() + " to Boolean");
758760
}
759-
} else {
761+
}
762+
else {
760763
throw new ClassCastException("Cannot automatically cast value of type " + value.getClass().getName() + " to " + expectedType.getName());
761764
}
762765
} catch (ClassCastException e) {
@@ -779,12 +782,12 @@ public int[] insertBatchData(String tableName, List<Map<String, Object>> dataRow
779782
throw new IllegalArgumentException("First data row map cannot be null or empty.");
780783
}
781784
Set<String> columnSet = new LinkedHashSet<>(firstRow.keySet());
782-
if (columnSet.isEmpty()) {
785+
if(columnSet.isEmpty()) {
783786
throw new IllegalArgumentException("No columns found in the first data row.");
784787
}
785788
List<String> columns = new ArrayList<>(columnSet);
786789
List<String> quotedColumns = new ArrayList<>();
787-
for (String col : columns) {
790+
for(String col : columns) {
788791
validateIdentifier(col);
789792
quotedColumns.add(quoteIdentifier(col));
790793
}
@@ -1100,87 +1103,124 @@ public boolean updateTableSchema(String tableName, List<String> newColumnDefs) {
11001103

11011104
public boolean updateTableSchema(String tableName, List<String> newColumnDefs, List<String> newConstraints) {
11021105
validateIdentifier(tableName);
1103-
if (newColumnDefs == null || newColumnDefs.isEmpty()) {
1104-
throw new IllegalArgumentException("At least one column definition is required.");
1106+
if (newColumnDefs == null) {
1107+
throw new IllegalArgumentException("Column definitions list cannot be null.");
11051108
}
11061109

1107-
try {
1108-
DatabaseMetaData metaData = getConnection().getMetaData();
1109-
List<String> currentColumns = getTableColumns(tableName, metaData);
1110+
List<String> actualColumnDefs = new ArrayList<>();
1111+
List<String> allConstraints = (newConstraints == null) ? new ArrayList<>() : new ArrayList<>(newConstraints);
1112+
Set<String> constraintKeywords = new HashSet<>(Arrays.asList("CONSTRAINT", "PRIMARY", "UNIQUE", "FOREIGN", "CHECK"));
1113+
1114+
for (String def : newColumnDefs) {
1115+
if (def == null || def.trim().isEmpty()) {
1116+
throw new IllegalArgumentException("Column definition or constraint cannot be null or empty.");
1117+
}
1118+
String firstWord = def.trim().split("[\\s(]+")[0].toUpperCase();
1119+
if (constraintKeywords.contains(firstWord)) {
1120+
allConstraints.add(def);
1121+
} else {
1122+
actualColumnDefs.add(def);
1123+
}
1124+
}
1125+
1126+
if (actualColumnDefs.isEmpty() && tableExists(tableName)) {
1127+
logger.warn("No column definitions provided for schema update of existing table '" + tableName + "'. Only constraints will be processed.");
1128+
} else if (actualColumnDefs.isEmpty()) {
1129+
throw new IllegalArgumentException("At least one column definition is required for a new table.");
1130+
}
11101131

1111-
if (currentColumns.isEmpty()) {
1132+
try {
1133+
if (!tableExists(tableName)) {
11121134
logger.warn("Table '" + tableName + "' does not exist. Creating it instead.");
1113-
createTable(tableName, newColumnDefs, newConstraints);
1135+
createTable(tableName, actualColumnDefs, allConstraints);
11141136
return true;
11151137
}
11161138

1117-
List<String> newColumnNames = new ArrayList<>();
1118-
Map<String, String> newColumnDefinitions = new HashMap<>();
1139+
DatabaseMetaData metaData = getConnection().getMetaData();
1140+
List<String> currentColumns = getTableColumns(tableName, metaData);
11191141

1120-
for (String colDef : newColumnDefs) {
1121-
if (colDef == null || colDef.trim().isEmpty()) {
1122-
throw new IllegalArgumentException("Column definition cannot be null or empty.");
1123-
}
1142+
List<String> newColumnNames = new ArrayList<>();
1143+
Map<String, String> newColumnDefinitions = new LinkedHashMap<>();
11241144

1145+
for (String colDef : actualColumnDefs) {
11251146
String[] parts = colDef.trim().split("\\s+", 2);
1126-
if (parts.length < 1) {
1127-
throw new IllegalArgumentException("Invalid column definition: " + colDef);
1128-
}
1129-
1130-
String columnName = parts[0];
1131-
if ((columnName.startsWith("\"") && columnName.endsWith("\"")) ||
1132-
(columnName.startsWith("`") && columnName.endsWith("`"))) {
1133-
columnName = columnName.substring(1, columnName.length() - 1);
1134-
}
1135-
1147+
String columnName = parts[0].replace("`", "").replace("\"", "");
11361148
newColumnNames.add(columnName);
11371149
newColumnDefinitions.put(columnName, colDef);
11381150
}
11391151

11401152
boolean changes = false;
11411153

1142-
for (String newCol : newColumnNames) {
1143-
if (!currentColumns.contains(newCol)) {
1144-
String addColumnSql = "ALTER TABLE " + quoteIdentifier(tableName) +
1145-
" ADD COLUMN " + newColumnDefinitions.get(newCol);
1146-
1147-
logger.debug("Adding column: " + addColumnSql);
1148-
try (Statement stmt = getConnection().createStatement()) {
1149-
stmt.execute(addColumnSql);
1150-
logger.info("Added column '" + newCol + "' to table '" + tableName + "'");
1151-
changes = true;
1154+
if (databaseType == DatabaseType.SQLITE) {
1155+
List<String> columnsToRemove = new ArrayList<>();
1156+
for (String oldCol : currentColumns) {
1157+
if (!newColumnNames.isEmpty() && !newColumnNames.contains(oldCol)) {
1158+
columnsToRemove.add(oldCol);
1159+
}
1160+
}
1161+
boolean hasNewColumns = false;
1162+
for (String newCol : newColumnNames) {
1163+
if (!currentColumns.contains(newCol)) {
1164+
hasNewColumns = true;
1165+
break;
11521166
}
11531167
}
1154-
}
11551168

1156-
if (databaseType != DatabaseType.SQLITE) {
1157-
for (String oldCol : currentColumns) {
1158-
if (!newColumnNames.contains(oldCol)) {
1159-
String dropColumnSql = "ALTER TABLE " + quoteIdentifier(tableName) +
1160-
" DROP COLUMN " + quoteIdentifier(oldCol);
1161-
1162-
logger.debug("Dropping column: " + dropColumnSql);
1163-
try (Statement stmt = getConnection().createStatement()) {
1164-
stmt.execute(dropColumnSql);
1165-
logger.info("Dropped column '" + oldCol + "' from table '" + tableName + "'");
1169+
if (!columnsToRemove.isEmpty() || !allConstraints.isEmpty()) {
1170+
recreateTableWithNewSchema(tableName, currentColumns, columnsToRemove, newColumnDefinitions, allConstraints);
1171+
return true;
1172+
}
1173+
if(hasNewColumns) {
1174+
for (String newCol : newColumnNames) {
1175+
if (!currentColumns.contains(newCol)) {
1176+
String addColumnSql = "ALTER TABLE " + quoteIdentifier(tableName) + " ADD COLUMN " + newColumnDefinitions.get(newCol);
1177+
logger.debug("Adding column: " + addColumnSql);
1178+
execute(addColumnSql);
11661179
changes = true;
11671180
}
11681181
}
11691182
}
1170-
} else {
1171-
List<String> columnsToRemove = new ArrayList<>();
1172-
for (String oldCol : currentColumns) {
1173-
if (!newColumnNames.contains(oldCol)) {
1174-
columnsToRemove.add(oldCol);
1175-
}
1183+
return changes;
1184+
}
1185+
1186+
for (String newCol : newColumnNames) {
1187+
if (!currentColumns.contains(newCol)) {
1188+
String addColumnSql = "ALTER TABLE " + quoteIdentifier(tableName) + " ADD COLUMN " + newColumnDefinitions.get(newCol);
1189+
logger.debug("Adding column: " + addColumnSql);
1190+
execute(addColumnSql);
1191+
changes = true;
11761192
}
1193+
}
11771194

1178-
if (!columnsToRemove.isEmpty()) {
1179-
recreateTableWithNewSchema(tableName, currentColumns, columnsToRemove, newColumnDefinitions, newConstraints);
1195+
for (String oldCol : currentColumns) {
1196+
if (!newColumnNames.isEmpty() && !newColumnNames.contains(oldCol)) {
1197+
String dropColumnSql = "ALTER TABLE " + quoteIdentifier(tableName) + " DROP COLUMN " + quoteIdentifier(oldCol);
1198+
logger.debug("Dropping column: " + dropColumnSql);
1199+
execute(dropColumnSql);
11801200
changes = true;
11811201
}
11821202
}
11831203

1204+
for (String constraint : allConstraints) {
1205+
String addConstraintSql = "ALTER TABLE " + quoteIdentifier(tableName) + " ADD " + constraint;
1206+
try {
1207+
logger.debug("Adding constraint: " + addConstraintSql);
1208+
execute(addConstraintSql);
1209+
changes = true;
1210+
} catch (RuntimeException e) {
1211+
if (e.getCause() instanceof SQLException) {
1212+
String msg = e.getCause().getMessage().toLowerCase();
1213+
if (msg.contains("duplicate") || msg.contains("already exist")) {
1214+
logger.debug("Constraint might already exist, skipping: " + constraint);
1215+
} else {
1216+
throw e;
1217+
}
1218+
} else {
1219+
throw e;
1220+
}
1221+
}
1222+
}
1223+
11841224
return changes;
11851225
} catch (SQLException e) {
11861226
logger.error("Failed to update table schema for '" + tableName + "': " + e.getMessage());
@@ -1193,7 +1233,7 @@ private void recreateTableWithNewSchema(String tableName, List<String> currentCo
11931233
List<String> newConstraints)
11941234
throws SQLException {
11951235

1196-
logger.debug("Recreating table '" + tableName + "' to remove columns: " + String.join(", ", columnsToRemove));
1236+
logger.debug("Recreating table '" + tableName + "' for schema update.");
11971237

11981238
boolean wasAutoCommit = getConnection().getAutoCommit();
11991239
if (wasAutoCommit) {
@@ -1203,27 +1243,28 @@ private void recreateTableWithNewSchema(String tableName, List<String> currentCo
12031243
try {
12041244
String tempTableName = tableName + "_temp_" + System.currentTimeMillis();
12051245

1206-
List<String> newTableColumns = new ArrayList<>();
1207-
for (String colName : newColumnDefinitions.keySet()) {
1208-
newTableColumns.add(newColumnDefinitions.get(colName));
1209-
}
1246+
List<String> newTableColumnDefs = new ArrayList<>(newColumnDefinitions.values());
12101247

1211-
createTable(tempTableName, newTableColumns, newConstraints);
1248+
createTable(tempTableName, newTableColumnDefs, newConstraints);
12121249

12131250
List<String> columnsToCopy = new ArrayList<>();
12141251
for (String col : currentColumns) {
1215-
if (!columnsToRemove.contains(col)) {
1252+
if (!columnsToRemove.contains(col) && newColumnDefinitions.containsKey(col.replace("`", "").replace("\"", ""))) {
12161253
columnsToCopy.add(quoteIdentifier(col));
12171254
}
12181255
}
12191256

1220-
String copyDataSql = "INSERT INTO " + quoteIdentifier(tempTableName) +
1221-
" SELECT " + String.join(", ", columnsToCopy) +
1222-
" FROM " + quoteIdentifier(tableName);
1257+
if (!columnsToCopy.isEmpty()) {
1258+
String joinedCols = String.join(", ", columnsToCopy);
1259+
String copyDataSql = "INSERT INTO " + quoteIdentifier(tempTableName) +
1260+
" (" + joinedCols + ")" +
1261+
" SELECT " + joinedCols +
1262+
" FROM " + quoteIdentifier(tableName);
12231263

1224-
logger.debug("Copying data: " + copyDataSql);
1225-
try (Statement stmt = getConnection().createStatement()) {
1226-
stmt.execute(copyDataSql);
1264+
logger.debug("Copying data: " + copyDataSql);
1265+
try (Statement stmt = getConnection().createStatement()) {
1266+
stmt.execute(copyDataSql);
1267+
}
12271268
}
12281269

12291270
deleteTable(tableName);
@@ -1238,7 +1279,6 @@ private void recreateTableWithNewSchema(String tableName, List<String> currentCo
12381279

12391280
if (wasAutoCommit) {
12401281
getConnection().commit();
1241-
getConnection().setAutoCommit(true);
12421282
}
12431283

12441284
logger.info("Successfully recreated table '" + tableName + "' with updated schema");
@@ -1249,15 +1289,15 @@ private void recreateTableWithNewSchema(String tableName, List<String> currentCo
12491289
logger.error("Failed to rollback transaction: " + rollbackEx.getMessage());
12501290
}
12511291

1292+
throw e;
1293+
} finally {
12521294
if (wasAutoCommit) {
12531295
try {
12541296
getConnection().setAutoCommit(true);
12551297
} catch (SQLException autoCommitEx) {
12561298
logger.error("Failed to restore autoCommit: " + autoCommitEx.getMessage());
12571299
}
12581300
}
1259-
1260-
throw e;
12611301
}
12621302
}
12631303
}

0 commit comments

Comments
 (0)