diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb index 49a842496..722262d7a 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -200,56 +200,59 @@ def columns(table_name) # end def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) - create_sequence = id != false - td = create_table_definition( - table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization) - ) + OracleEnhancedAdapter.using_identity(options[:primary_key_as_identity]) do + create_sequence = id != false + td = create_table_definition( + table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization) + ) - if id && !td.as - pk = primary_key || Base.get_primary_key(table_name.to_s.singularize) + if id && !td.as + pk = primary_key || Base.get_primary_key(table_name.to_s.singularize) - if pk.is_a?(Array) - td.primary_keys pk - else - td.primary_key pk, id, **options + if pk.is_a?(Array) + td.primary_keys pk + else + td.primary_key pk, id, **options + end end - end - # store that primary key was defined in create_table block - unless create_sequence - class << td - attr_accessor :create_sequence - def primary_key(*args) - self.create_sequence = true - super(*args) + # store that primary key was defined in create_table block + unless create_sequence + class << td + attr_accessor :create_sequence + def primary_key(name, type = :primary_key, **options) + self.create_sequence = true + + super(name, type, **options) + end end end - end - yield td if block_given? - create_sequence = create_sequence || td.create_sequence + yield td if block_given? + create_sequence = create_sequence || td.create_sequence - if force && data_source_exists?(table_name) - drop_table(table_name, force: force, if_exists: true) - else - schema_cache.clear_data_source_cache!(table_name.to_s) - end + if force && data_source_exists?(table_name) + drop_table(table_name, force: force, if_exists: true) + else + schema_cache.clear_data_source_cache!(table_name.to_s) + end - execute schema_creation.accept td + execute schema_creation.accept td - create_sequence_and_trigger(table_name, options) if create_sequence + create_sequence_and_trigger(table_name, options) if create_sequence - if supports_comments? && !supports_comments_in_create? - if table_comment = td.comment.presence - change_table_comment(table_name, table_comment) - end - td.columns.each do |column| - change_column_comment(table_name, column.name, column.comment) if column.comment.present? + if supports_comments? && !supports_comments_in_create? + if table_comment = td.comment.presence + change_table_comment(table_name, table_comment) + end + td.columns.each do |column| + change_column_comment(table_name, column.name, column.comment) if column.comment.present? + end end - end - td.indexes.each { |c, o| add_index table_name, c, **o } + td.indexes.each { |c, o| add_index table_name, c, **o } - rebuild_primary_key_index_to_default_tablespace(table_name, options) + rebuild_primary_key_index_to_default_tablespace(table_name, options) + end end def rename_table(table_name, new_name) # :nodoc: @@ -413,14 +416,16 @@ def add_reference(table_name, ref_name, **options) end def add_column(table_name, column_name, type, **options) # :nodoc: - type = aliased_types(type.to_s, type) - at = create_alter_table table_name - at.add_column(column_name, type, **options) - add_column_sql = schema_creation.accept at - add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) - execute add_column_sql - create_sequence_and_trigger(table_name, options) if type && type.to_sym == :primary_key - change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + OracleEnhancedAdapter.using_identity(options[:identity]) do + type = aliased_types(type.to_s, type) + at = create_alter_table table_name + at.add_column(column_name, type, **options) + add_column_sql = schema_creation.accept at + add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) + execute add_column_sql + create_sequence_and_trigger(table_name, options) if type && type.to_sym == :primary_key + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + end ensure clear_table_columns_cache(table_name) end @@ -535,11 +540,13 @@ def column_comment(table_name, column_name) # :nodoc: end # Maps logical Rails types to Oracle-specific data types. - def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc: - # Ignore options for :text, :ntext and :binary columns - return super(type) if ["text", "ntext", "binary"].include?(type.to_s) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, identity: nil, **) # :nodoc: + OracleEnhancedAdapter.using_identity(identity) do + # Ignore options for :text, :ntext and :binary columns + return super(type) if ["text", "ntext", "binary"].include?(type.to_s) - super + super + end end def tablespace(table_name) @@ -702,6 +709,8 @@ def column_for(table_name, column_name) def create_sequence_and_trigger(table_name, options) # TODO: Needs rename since no triggers created # This method will be removed since sequence will not be created separately + return if OracleEnhancedAdapter.use_identity_for_pk + seq_name = options[:sequence_name] || default_sequence_name(table_name) seq_start_value = options[:sequence_start_value] || default_sequence_start_value execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}" diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index 2c561fd24..57240c428 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -229,6 +229,14 @@ class OracleEnhancedAdapter < AbstractAdapter cattr_accessor :permissions self.permissions = ["unlimited tablespace", "create session", "create table", "create view", "create sequence"] + ## + # :singleton-method: + # To generate primary key columns using IDENTITY: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.use_identity_for_pk = true + cattr_accessor :use_identity_for_pk + self.use_identity_for_pk = false + ## # :singleton-method: # Specify default sequence start with value (by default 1 if not explicitly set), e.g.: @@ -409,10 +417,17 @@ def supports_longer_identifier? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge( boolean: { name: "VARCHAR2", limit: 1 } ) + # if use_identity_for_pk then generate primary key as IDENTITY + NATIVE_DATABASE_TYPES_IDENTITY_PK = { + primary_key: "NUMBER(38) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY" + } # :startdoc: def native_database_types # :nodoc: - emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES + types = emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES + types = types.merge(NATIVE_DATABASE_TYPES_IDENTITY_PK) if use_identity_for_pk + + types end # CONNECTION MANAGEMENT ==================================== @@ -476,14 +491,25 @@ def discard! # called directly; used by ActiveRecord to get the next primary key value # when inserting a new database record (see #prefetch_primary_key?). def next_sequence_value(sequence_name) - # if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger - raise ArgumentError.new "Trigger based primary key is not supported" if sequence_name == AUTOGENERATED_SEQUENCE_NAME + # if sequence_name is set to :autogenerated it means that primary key will be populated by an identity sequence + return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME + # call directly connection method to avoid prepared statement which causes fetching of next sequence value twice select_value(<<~SQL.squish, "SCHEMA") SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual SQL end + # Helper method for temporarily changing the value of OracleEnhancedAdapter.use_identity_for_pk (e.g., for a + # single create_table block) + def self.using_identity(value = nil, &block) + previous_value = self.use_identity_for_pk + self.use_identity_for_pk = value.nil? ? self.use_identity_for_pk : value + yield + ensure + self.use_identity_for_pk = previous_value + end + # Returns true for Oracle adapter (since Oracle requires primary key # values to be pre-fetched before insert). See also #next_sequence_value. def prefetch_primary_key?(table_name = nil) diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb index 4f4a346f8..7d3939014 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb @@ -340,52 +340,52 @@ class ::TestEmployee < ActiveRecord::Base; end end describe "rename index" do - before(:each) do - @conn = ActiveRecord::Base.connection - schema_define do - create_table :test_employees do |t| - t.string :first_name - t.string :last_name + before(:each) do + @conn = ActiveRecord::Base.connection + schema_define do + create_table :test_employees do |t| + t.string :first_name + t.string :last_name + end + add_index :test_employees, :first_name end - add_index :test_employees, :first_name + class ::TestEmployee < ActiveRecord::Base; end end - class ::TestEmployee < ActiveRecord::Base; end - end - after(:each) do - schema_define do - drop_table :test_employees + after(:each) do + schema_define do + drop_table :test_employees + end + Object.send(:remove_const, "TestEmployee") + ActiveRecord::Base.clear_cache! end - Object.send(:remove_const, "TestEmployee") - ActiveRecord::Base.clear_cache! - end - it "should raise error when current index name and new index name are identical" do - expect do - @conn.rename_index("test_employees", "i_test_employees_first_name", "i_test_employees_first_name") - end.to raise_error(ActiveRecord::StatementInvalid) - end + it "should raise error when current index name and new index name are identical" do + expect do + @conn.rename_index("test_employees", "i_test_employees_first_name", "i_test_employees_first_name") + end.to raise_error(ActiveRecord::StatementInvalid) + end - it "should raise error when new index name length is too long" do - skip if @oracle12cr2_or_higher - expect do - @conn.rename_index("test_employees", "i_test_employees_first_name", "a" * 31) - end.to raise_error(ArgumentError) - end + it "should raise error when new index name length is too long" do + skip if @oracle12cr2_or_higher + expect do + @conn.rename_index("test_employees", "i_test_employees_first_name", "a" * 31) + end.to raise_error(ArgumentError) + end - it "should raise error when current index name does not exist" do - expect do - @conn.rename_index("test_employees", "nonexist_index_name", "new_index_name") - end.to raise_error(ActiveRecord::StatementInvalid) - end + it "should raise error when current index name does not exist" do + expect do + @conn.rename_index("test_employees", "nonexist_index_name", "new_index_name") + end.to raise_error(ActiveRecord::StatementInvalid) + end - it "should rename index name with new one" do - skip if @oracle12cr2_or_higher - expect do - @conn.rename_index("test_employees", "i_test_employees_first_name", "new_index_name") - end.not_to raise_error + it "should rename index name with new one" do + skip if @oracle12cr2_or_higher + expect do + @conn.rename_index("test_employees", "i_test_employees_first_name", "new_index_name") + end.not_to raise_error + end end -end describe "ignore options for LOB columns" do after(:each) do @@ -622,9 +622,9 @@ class ::TestPost < ActiveRecord::Base index_name = @conn.select_value( "SELECT index_name FROM all_constraints - WHERE table_name = 'TEST_POSTS' - AND constraint_type = 'P' - AND owner = SYS_CONTEXT('userenv', 'current_schema')") + WHERE table_name = 'TEST_POSTS' + AND constraint_type = 'P' + AND owner = SYS_CONTEXT('userenv', 'current_schema')") expect(TestPost.connection.select_value("SELECT tablespace_name FROM user_indexes WHERE index_name = '#{index_name}'")).to eq("USERS") end @@ -637,9 +637,9 @@ class ::TestPost < ActiveRecord::Base index_name = @conn.select_value( "SELECT index_name FROM all_constraints - WHERE table_name = 'TEST_POSTS' - AND constraint_type = 'P' - AND owner = SYS_CONTEXT('userenv', 'current_schema')") + WHERE table_name = 'TEST_POSTS' + AND constraint_type = 'P' + AND owner = SYS_CONTEXT('userenv', 'current_schema')") expect(TestPost.connection.select_value("SELECT tablespace_name FROM user_indexes WHERE index_name = '#{index_name}'")).to eq(DATABASE_NON_DEFAULT_TABLESPACE) end @@ -1046,7 +1046,7 @@ class ::TestFraction < ActiveRecord::Base it "should change virtual column definition" do schema_define do change_column :test_fractions, :percent, :virtual, - as: "ROUND((numerator/NULLIF(denominator,0))*100, 2)", type: :decimal, precision: 15, scale: 2 + as: "ROUND((numerator/NULLIF(denominator,0))*100, 2)", type: :decimal, precision: 15, scale: 2 end TestFraction.reset_column_information tf = TestFraction.columns.detect { |c| c.name == "percent" } @@ -1249,4 +1249,67 @@ class << @conn ActiveRecord::SchemaMigration.drop_table end end + + describe "identity columns" do + before(:all) do + @conn = ActiveRecord::Base.connection + schema_define do + end + end + + before(:each) do + @conn.instance_variable_set :@would_execute_sql, @would_execute_sql = +"" + class << @conn + def execute(sql, name = nil); @would_execute_sql << sql << ";\n"; end + end + end + + after(:each) do + class << @conn + remove_method :execute + end + @conn.instance_eval { remove_instance_variable :@would_execute_sql } + end + + it "should create an identity column when primary_key_as_identity: true is used" do + schema_define do + create_table :identity_test_table, primary_key_as_identity: true + end + expect(@would_execute_sql).to match(/CREATE +TABLE .* GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY/) + end + + it 'should create an identity column when identity: true is used with t.primary_key' do + schema_define do + create_table :identity_test_table, id: false do |t| + t.primary_key :test, :primary_key, identity: true + end + end + expect(@would_execute_sql).to match(/CREATE +TABLE .* GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY/) + end + + it 'should create an identity column when identity: true is used with add_column' do + schema_define do + add_column :identity_test_table, :test_id, :primary_key, identity: true + end + expect(@would_execute_sql).to match(/ALTER +TABLE .* GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY/) + end + + it 'should create an identity column when primary_key_as_identity: true is used with a custom primary_key column' do + schema_define do + create_table :identity_test_table, id: false, primary_key_as_identity: true do |t| + t.primary_key :test, :primary_key + end + end + expect(@would_execute_sql).to match(/CREATE +TABLE .* GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY/) + end + + it 'should create an identity column with primary_key_as_identity: false and identity: true' do + schema_define do + create_table :identity_test_table, id: false, primary_key_as_identity: false do |t| + t.primary_key :test, :primary_key, identity: true + end + end + expect(@would_execute_sql).to match(/CREATE +TABLE .* GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL PRIMARY KEY/) + end + end end