@@ -169,11 +169,12 @@ private void committedCleanupFailure(String operation, Exception error) {
169169 /** @return true when the row already existed and still needs the batch UPDATE. */
170170 private boolean ensureRow (Connection connection , Map <String , DataValue > updates ) throws SQLException {
171171 if (dialect != Dialect .SQLITE && rowExists (connection )) return true ;
172+ Map <String , SqlUserSchema .ColumnDefinition > definitions = retainedDefinitions (connection , updates );
172173 StringBuilder names = new StringBuilder (quote (SqlUserSchema .UUID_COLUMN ));
173174 StringBuilder parameters = new StringBuilder ("?" );
174175 for (String key : updates .keySet ()) {
175176 names .append (", " ).append (quote (key ));
176- parameters .append (", " ).append (parameterExpression (schema . column (key )));
177+ parameters .append (", " ).append (parameterExpression (definitions . get (key )));
177178 }
178179 String prefix = dialect == Dialect .SQLITE ? "INSERT OR IGNORE INTO " : "INSERT INTO " ;
179180 String sql = prefix + quote (tableName ) + " (" + names + ") VALUES (" + parameters + ")" ;
@@ -182,7 +183,7 @@ private boolean ensureRow(Connection connection, Map<String, DataValue> updates)
182183 try (PreparedStatement statement = connection .prepareStatement (sql )) {
183184 dialect .bindUuid (statement , 1 , uuid );
184185 int index = 2 ;
185- for (Map .Entry <String , DataValue > entry : updates .entrySet ()) bind (statement , index ++, entry .getValue (), schema . column (entry .getKey ()));
186+ for (Map .Entry <String , DataValue > entry : updates .entrySet ()) bind (statement , index ++, entry .getValue (), definitions . get (entry .getKey ()));
186187 inserted = statement .executeUpdate ();
187188 } catch (SQLException insertFailure ) {
188189 if (dialect == Dialect .MYSQL && isDuplicateKey (insertFailure ) && rowExists (connection )) return true ;
@@ -204,18 +205,18 @@ private boolean rowExists(Connection connection) throws SQLException {
204205 }
205206
206207 private void updateValues (Connection connection , Map <String , DataValue > updates ) throws SQLException {
208+ Map <String , SqlUserSchema .ColumnDefinition > definitions = retainedDefinitions (connection , updates );
207209 StringBuilder sql = new StringBuilder ("UPDATE " ).append (quote (tableName )).append (" SET " );
208210 boolean first = true ;
209211 for (String key : updates .keySet ()) {
210212 if (!first ) sql .append (", " );
211213 first = false ;
212- SqlUserSchema .ColumnDefinition definition = schema .column (key );
213- sql .append (quote (key )).append ('=' ).append (parameterExpression (definition ));
214+ sql .append (quote (key )).append ('=' ).append (parameterExpression (definitions .get (key )));
214215 }
215216 sql .append (" WHERE " ).append (quote (SqlUserSchema .UUID_COLUMN )).append ("=?" );
216217 try (PreparedStatement statement = connection .prepareStatement (sql .toString ())) {
217218 int index = 1 ;
218- for (Map .Entry <String , DataValue > entry : updates .entrySet ()) bind (statement , index ++, entry .getValue (), schema . column (entry .getKey ()));
219+ for (Map .Entry <String , DataValue > entry : updates .entrySet ()) bind (statement , index ++, entry .getValue (), definitions . get (entry .getKey ()));
219220 dialect .bindUuid (statement , index , uuid ); statement .executeUpdate ();
220221 }
221222 }
@@ -270,26 +271,92 @@ else if (value.isBoolean()) {
270271 } else statement .setObject (index , value .toString ());
271272 }
272273
274+ private BooleanStorage booleanStorage (String sqlType ) {
275+ String normalized = sqlType .strip ().toUpperCase (Locale .ROOT );
276+ if (startsType (normalized , "BOOLEAN" ) || startsType (normalized , "BOOL" )) return BooleanStorage .NATIVE ;
277+ if (startsType (normalized , "BIT" ) || startsType (normalized , "VARBIT" )) return dialect == Dialect .POSTGRESQL ? BooleanStorage .POSTGRES_BIT : BooleanStorage .NUMERIC ;
278+ if (startsType (normalized , "TINYINT" ) || startsType (normalized , "SMALLINT" ) || startsType (normalized , "MEDIUMINT" ) || startsType (normalized , "INT" ) || startsType (normalized , "INTEGER" ) || startsType (normalized , "BIGINT" )) return BooleanStorage .NUMERIC ;
279+ return BooleanStorage .TEXT ;
280+ }
281+
273282 private String parameterExpression (SqlUserSchema .ColumnDefinition definition ) {
274- if (booleanStorage (definition ) != BooleanStorage .POSTGRES_BIT ) return "?" ;
283+ BooleanStorage storage = booleanStorage (definition );
284+ if (storage != BooleanStorage .POSTGRES_BIT ) return "?" ;
275285 return "CAST(? AS " + postgresBitType (definition ) + ")" ;
276286 }
277287
288+ private Map <String , SqlUserSchema .ColumnDefinition > retainedDefinitions (Connection connection , Map <String , DataValue > updates ) throws SQLException {
289+ Map <String , SqlUserSchema .ColumnDefinition > definitions = new HashMap <>();
290+ for (String key : updates .keySet ()) {
291+ SqlUserSchema .ColumnDefinition definition = schema .column (key );
292+ definitions .put (key , retainedDefinition (connection , definition ));
293+ }
294+ return definitions ;
295+ }
296+
297+ private SqlUserSchema .ColumnDefinition retainedDefinition (Connection connection , SqlUserSchema .ColumnDefinition definition ) throws SQLException {
298+ if (definition == null || definition .dataType () != DataType .BOOLEAN || dialect != Dialect .POSTGRESQL ) return definition ;
299+ java .sql .DatabaseMetaData metadata = connection .getMetaData ();
300+ if (metadata == null ) return definition ;
301+ try (ResultSet columns = metadata .getColumns (null , metadataSchema (connection ), tableName , definition .name ())) {
302+ if (columns .next ()) {
303+ String type = columns .getString ("TYPE_NAME" );
304+ if (type != null && !type .isBlank ()) {
305+ String normalized = type .strip ().toUpperCase (Locale .ROOT );
306+ if (startsType (normalized , "BIT" ) || startsType (normalized , "VARBIT" )) {
307+ int width = columns .getInt ("COLUMN_SIZE" );
308+ if (columns .wasNull () || width <= 0 ) width = 1 ;
309+ // PgJDBC exposes this as VARBIT on supported versions, but
310+ // accept the SQL spelling too so preserving a legacy column
311+ // does not accidentally turn BIT VARYING(n) into BIT(n).
312+ String retainedType = postgresVaryingBit (normalized )
313+ ? "BIT VARYING(" + width + ")" : "BIT(" + width + ")" ;
314+ return new SqlUserSchema .ColumnDefinition (definition .name (), retainedType , DataType .BOOLEAN );
315+ }
316+ // The current logical schema may say BOOLEAN while an existing
317+ // server still has a VARCHAR or numeric column. Bind according
318+ // to the retained physical type instead of sending a typed
319+ // boolean that PostgreSQL cannot assign to that column.
320+ return new SqlUserSchema .ColumnDefinition (definition .name (), type , DataType .BOOLEAN );
321+ }
322+ }
323+ }
324+ return definition ;
325+ }
326+
327+ private boolean postgresVaryingBit (String normalizedType ) {
328+ return startsType (normalizedType , "VARBIT" ) || normalizedType .matches ("^BIT\\ s+VARYING(?:\\ (\\ d+\\ ))?(?:\\ s+.*)?$" );
329+ }
330+
331+ private String metadataSchema (Connection connection ) throws SQLException {
332+ if (dialect != Dialect .POSTGRESQL ) return null ;
333+ String regclass = '"' + tableName .replace ("\" " , "\" \" " ) + '"' ;
334+ String sql = "SELECT n.nspname FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
335+ + "ON n.oid=c.relnamespace WHERE c.oid=pg_catalog.to_regclass(?)" ;
336+ try (PreparedStatement statement = connection .prepareStatement (sql )) {
337+ statement .setString (1 , regclass );
338+ try (ResultSet result = statement .executeQuery ()) {
339+ return result .next () ? result .getString (1 ) : null ;
340+ }
341+ }
342+ }
343+
278344 private String postgresBitType (SqlUserSchema .ColumnDefinition definition ) {
279345 String sqlType = definition .sqlType ().strip ();
280- int separator = sqlType .indexOf (' ' );
281- String token = separator < 0 ? sqlType : sqlType .substring (0 , separator );
282- if (!token .matches ("(?i)BIT(?:\\ (\\ d+\\ ))?" )) {
346+ java .util .regex .Matcher type = java .util .regex .Pattern
347+ .compile ("(?i)^(BIT(?:\\ (\\ d+\\ )|\\ s+VARYING(?:\\ (\\ d+\\ ))?)?)(?:\\ s+.*)?$" )
348+ .matcher (sqlType );
349+ if (!type .matches ()) {
283350 throw new IllegalArgumentException ("Unsupported PostgreSQL bit type: " + definition .sqlType ());
284351 }
285- return token .toUpperCase (Locale .ROOT );
352+ return type . group ( 1 ) .toUpperCase (Locale .ROOT );
286353 }
287354
288355 private BooleanStorage booleanStorage (SqlUserSchema .ColumnDefinition definition ) {
289356 if (definition == null || definition .dataType () != DataType .BOOLEAN ) return BooleanStorage .TEXT ;
290357 String sqlType = definition .sqlType ().strip ().toUpperCase (Locale .ROOT );
291358 if (startsType (sqlType , "BOOLEAN" ) || startsType (sqlType , "BOOL" )) return BooleanStorage .NATIVE ;
292- if (startsType (sqlType , "BIT" )) return dialect == Dialect .POSTGRESQL ? BooleanStorage .POSTGRES_BIT : BooleanStorage .NUMERIC ;
359+ if (startsType (sqlType , "BIT" ) || startsType ( sqlType , "VARBIT" ) ) return dialect == Dialect .POSTGRESQL ? BooleanStorage .POSTGRES_BIT : BooleanStorage .NUMERIC ;
293360 if (startsType (sqlType , "TINYINT" ) || startsType (sqlType , "SMALLINT" ) || startsType (sqlType , "MEDIUMINT" ) || startsType (sqlType , "INT" ) || startsType (sqlType , "INTEGER" ) || startsType (sqlType , "BIGINT" )) return BooleanStorage .NUMERIC ;
294361 return BooleanStorage .TEXT ;
295362 }
0 commit comments