11package io .github .intisy .utils .database ;
2+
23import io .github .intisy .simple .logger .EmptyLogger ;
34import io .github .intisy .simple .logger .SimpleLogger ;
45
56import java .io .File ;
67import java .sql .*;
78import java .util .*;
89import java .util .regex .Pattern ;
10+
911@ SuppressWarnings ({"unused" , "SqlNoDataSourceInspection" , "SqlSourceToSinkFlow" })
1012public 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