Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional identity columns #2201

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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