From 0f6eb27c8d4e751b80e5c88d784f3a2a95a6dc70 Mon Sep 17 00:00:00 2001 From: lhimo <36077138+lhimo@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:00:47 +0100 Subject: [PATCH] fix: scope SchemaCreation to IBM_DBAdapter to prevent breaking other adapters The SchemaCreation class was previously defined by reopening the base ActiveRecord::ConnectionAdapters::SchemaCreation class directly. This caused IBM DB2-specific methods (e.g. visit_TableDefinition calling @conn.servertype) to be invoked for ALL database adapters, not just IBM DB2 connections. When an application uses a non-IBM adapter (e.g. PostgreSQL) alongside the ibm_db gem, Rails' pending migration check triggers the overridden visit_TableDefinition, which calls @conn.servertype on a PostgreSQLAdapter instance, raising: NoMethodError: undefined method 'servertype' for an instance of ActiveRecord::ConnectionAdapters::PostgreSQLAdapter This fix moves SchemaCreation to be a proper subclass nested inside IBM_DBAdapter (following the same pattern used by PostgreSQL, MySQL, and SQLite adapters in Rails) and adds a schema_creation method override so the IBM-specific SchemaCreation is only used when the active connection is an IBM DB2 connection. Co-Authored-By: Claude Opus 4.6 Signed-off-by: lhimo <36077138+lhimo@users.noreply.github.com> --- .../connection_adapters/ibm_db_adapter.rb | 219 +++++++++--------- 1 file changed, 112 insertions(+), 107 deletions(-) diff --git a/IBM_DB_Adapter/ibm_db/lib/active_record/connection_adapters/ibm_db_adapter.rb b/IBM_DB_Adapter/ibm_db/lib/active_record/connection_adapters/ibm_db_adapter.rb index 0e09358..0d5e3c0 100644 --- a/IBM_DB_Adapter/ibm_db/lib/active_record/connection_adapters/ibm_db_adapter.rb +++ b/IBM_DB_Adapter/ibm_db/lib/active_record/connection_adapters/ibm_db_adapter.rb @@ -186,113 +186,6 @@ def unique_constraints_in_create(table, stream) end end - class SchemaCreation - private - - def visit_TableDefinition(o) - create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE " - create_sql << 'IF NOT EXISTS ' if o.if_not_exists - create_sql << "#{quote_table_name(o.name)} " - - statements = o.columns.map { |c| accept c } - statements << accept(o.primary_keys) if o.primary_keys - - if supports_indexes_in_create? - statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) - end - - statements.concat(o.foreign_keys.map { |fk| accept fk }) if use_foreign_keys? - - statements.concat(o.check_constraints.map { |chk| accept chk }) if supports_check_constraints? - - @conn.puts_log "visit_TableDefinition #{@conn.servertype}" - if !@conn.servertype.instance_of? IBM_IDS - statements.concat(o.unique_constraints.map { |exc| accept exc }) if supports_unique_constraints? - end - - create_sql << "(#{statements.join(', ')})" if statements.present? - add_table_options!(create_sql, o) - create_sql << " AS (#{to_sql(o.as)}) WITH DATA" if o.as - create_sql - end - - def visit_ColumnDefinition(o) - if @conn.instance_of? IBM_DBAdapter - @conn.puts_log "visit_ColumnDefinition #{o.name} #{o} #{@conn} #{@conn.servertype}" - end - o.sql_type = type_to_sql(o.type, **o.options) - column_sql = +"#{quote_column_name(o.name)} #{o.sql_type}" - add_column_options!(column_sql, column_options(o)) - column_sql - end - - def add_column_options!(sql, options) - if options_include_default?(options) - sql << " DEFAULT #{quote_default_expression(options[:default], - options[:column])}" - end - sql << ' GENERATED BY DEFAULT AS IDENTITY (START WITH 1000)' if options[:auto_increment] == true - sql << ' PRIMARY KEY' if options[:primary_key] == true - # must explicitly check for :null to allow change_column to work on migrations - sql << ' NOT NULL' if options[:null] == false - sql - end - - def visit_AlterTable(o) - sql = +"ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| accept col }.join(" ") - sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") - sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") - sql << o.check_constraint_adds.map { |con| visit_AddCheckConstraint con }.join(" ") - sql << o.check_constraint_drops.map { |con| visit_DropCheckConstraint con }.join(" ") - sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") - sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ") - sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ") - sql << o.unique_constraint_adds.map { |con| visit_AddUniqueConstraint con }.join(" ") - sql << o.unique_constraint_drops.map { |con| visit_DropUniqueConstraint con }.join(" ") - end - - def visit_ValidateConstraint(name) - "VALIDATE CONSTRAINT #{quote_column_name(name)}" - end - - def visit_UniqueConstraintDefinition(o) - column_name = Array(o.column).map { |column| quote_column_name(column) }.join(", ") - - sql = ["CONSTRAINT"] - sql << quote_column_name(o.name) - sql << "UNIQUE" - - if o.using_index - sql << "USING INDEX #{quote_column_name(o.using_index)}" - else - sql << "(#{column_name})" - end - -# if o.deferrable -# sql << "DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" -# end - - sql.join(" ") - end - - def visit_AddExclusionConstraint(o) - "ADD #{accept(o)}" - end - - def visit_DropExclusionConstraint(name) - "DROP CONSTRAINT #{quote_column_name(name)}" - end - - def visit_AddUniqueConstraint(o) - "ADD #{accept(o)}" - end - - def visit_DropUniqueConstraint(name) - "DROP CONSTRAINT #{quote_column_name(name)}" - end - - end end class Base @@ -659,6 +552,118 @@ class IBM_DBAdapter < AbstractAdapter :set_quoted_literal_replacement attr_accessor :sql, :handle_lobs_triggered, :sql_parameter_values + # IBM DB2-specific SchemaCreation subclass. + # This is scoped to IBM_DBAdapter so it does not interfere with other + # adapters (e.g. PostgreSQL, MySQL) that may coexist in the same + # application via Rails' multi-database support. + class SchemaCreation < ConnectionAdapters::SchemaCreation + private + + def visit_TableDefinition(o) + create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE " + create_sql << 'IF NOT EXISTS ' if o.if_not_exists + create_sql << "#{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + if supports_indexes_in_create? + statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) + end + + statements.concat(o.foreign_keys.map { |fk| accept fk }) if use_foreign_keys? + + statements.concat(o.check_constraints.map { |chk| accept chk }) if supports_check_constraints? + + @conn.puts_log "visit_TableDefinition #{@conn.servertype}" + if !@conn.servertype.instance_of? IBM_IDS + statements.concat(o.unique_constraints.map { |exc| accept exc }) if supports_unique_constraints? + end + + create_sql << "(#{statements.join(', ')})" if statements.present? + add_table_options!(create_sql, o) + create_sql << " AS (#{to_sql(o.as)}) WITH DATA" if o.as + create_sql + end + + def visit_ColumnDefinition(o) + @conn.puts_log "visit_ColumnDefinition #{o.name} #{o} #{@conn} #{@conn.servertype}" + o.sql_type = type_to_sql(o.type, **o.options) + column_sql = +"#{quote_column_name(o.name)} #{o.sql_type}" + add_column_options!(column_sql, column_options(o)) + column_sql + end + + def add_column_options!(sql, options) + if options_include_default?(options) + sql << " DEFAULT #{quote_default_expression(options[:default], + options[:column])}" + end + sql << ' GENERATED BY DEFAULT AS IDENTITY (START WITH 1000)' if options[:auto_increment] == true + sql << ' PRIMARY KEY' if options[:primary_key] == true + # must explicitly check for :null to allow change_column to work on migrations + sql << ' NOT NULL' if options[:null] == false + sql + end + + def visit_AlterTable(o) + sql = +"ALTER TABLE #{quote_table_name(o.name)} " + sql << o.adds.map { |col| accept col }.join(" ") + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") + sql << o.check_constraint_adds.map { |con| visit_AddCheckConstraint con }.join(" ") + sql << o.check_constraint_drops.map { |con| visit_DropCheckConstraint con }.join(" ") + sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") + sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ") + sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ") + sql << o.unique_constraint_adds.map { |con| visit_AddUniqueConstraint con }.join(" ") + sql << o.unique_constraint_drops.map { |con| visit_DropUniqueConstraint con }.join(" ") + end + + def visit_ValidateConstraint(name) + "VALIDATE CONSTRAINT #{quote_column_name(name)}" + end + + def visit_UniqueConstraintDefinition(o) + column_name = Array(o.column).map { |column| quote_column_name(column) }.join(", ") + + sql = ["CONSTRAINT"] + sql << quote_column_name(o.name) + sql << "UNIQUE" + + if o.using_index + sql << "USING INDEX #{quote_column_name(o.using_index)}" + else + sql << "(#{column_name})" + end + + sql.join(" ") + end + + def visit_AddExclusionConstraint(o) + "ADD #{accept(o)}" + end + + def visit_DropExclusionConstraint(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + + def visit_AddUniqueConstraint(o) + "ADD #{accept(o)}" + end + + def visit_DropUniqueConstraint(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + end + + # Returns the IBM DB2-specific SchemaCreation instance, ensuring that + # IBM-specific schema methods are only applied to IBM DB2 connections + # and do not affect other adapters. + def schema_creation # :nodoc: + SchemaCreation.new(self) + end + # Name of the adapter def adapter_name 'IBM_DB'