Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}"
Expand Down
32 changes: 29 additions & 3 deletions lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.:
Expand Down Expand Up @@ -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 ====================================
Expand Down Expand Up @@ -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)
Expand Down
Loading