Skip to content

Commit a4daa62

Browse files
committed
Revert "Merge pull request rsim#1669 from yahonda/drop_trigger_based_primary_key_support"
This reverts commit 7fc1f99, reversing changes made to 4630a39.
1 parent b13ae02 commit a4daa62

File tree

7 files changed

+315
-9
lines changed

7 files changed

+315
-9
lines changed

lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def tables(stream)
2121
# add table prefix or suffix for schema_migrations
2222
next if ignored? tbl
2323
table(tbl, stream)
24+
# add primary key trigger if table has it
25+
primary_key_trigger(tbl, stream)
2426
end
2527
# following table definitions
2628
# add foreign keys if table has them
@@ -33,6 +35,15 @@ def tables(stream)
3335
synonyms(stream)
3436
end
3537

38+
def primary_key_trigger(table_name, stream)
39+
if @connection.has_primary_key_trigger?(table_name)
40+
pk, _pk_seq = @connection.pk_and_sequence_for(table_name)
41+
stream.print " add_primary_key_trigger #{table_name.inspect}"
42+
stream.print ", primary_key: \"#{pk}\"" if pk != "id"
43+
stream.print "\n\n"
44+
end
45+
end
46+
3647
def synonyms(stream)
3748
syns = @connection.synonyms
3849
syns.each do |syn|

lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb

+24-2
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,33 @@ def column_for(table_name, column_name)
700700
end
701701

702702
def create_sequence_and_trigger(table_name, options)
703-
# TODO: Needs rename since no triggers created
704-
# This method will be removed since sequence will not be created separately
705703
seq_name = options[:sequence_name] || default_sequence_name(table_name)
706704
seq_start_value = options[:sequence_start_value] || default_sequence_start_value
707705
execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}"
706+
707+
create_primary_key_trigger(table_name, options) if options[:primary_key_trigger]
708+
end
709+
710+
def create_primary_key_trigger(table_name, options)
711+
seq_name = options[:sequence_name] || default_sequence_name(table_name)
712+
trigger_name = options[:trigger_name] || default_trigger_name(table_name)
713+
primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)
714+
execute <<-SQL
715+
CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
716+
BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW
717+
BEGIN
718+
IF inserting THEN
719+
IF :new.#{quote_column_name(primary_key)} IS NULL THEN
720+
SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual;
721+
END IF;
722+
END IF;
723+
END;
724+
SQL
725+
end
726+
727+
def default_trigger_name(table_name)
728+
# truncate table name if necessary to fit in max length of identifier
729+
"#{table_name.to_s[0, table_name_length - 4]}_pkt"
708730
end
709731

710732
def rebuild_primary_key_index_to_default_tablespace(table_name, options)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
module OracleEnhanced
6+
module SchemaStatementsExt
7+
# Create primary key trigger (so that you can skip primary key value in INSERT statement).
8+
# By default trigger name will be "table_name_pkt", you can override the name with
9+
# :trigger_name option (but it is not recommended to override it as then this trigger will
10+
# not be detected by ActiveRecord model and it will still do prefetching of sequence value).
11+
#
12+
# add_primary_key_trigger :users
13+
#
14+
# You can also create primary key trigger using +create_table+ with :primary_key_trigger
15+
# option:
16+
#
17+
# create_table :users, :primary_key_trigger => true do |t|
18+
# # ...
19+
# end
20+
#
21+
def add_primary_key_trigger(table_name, options = {})
22+
# call the same private method that is used for create_table :primary_key_trigger => true
23+
create_primary_key_trigger(table_name, options)
24+
end
25+
end
26+
end
27+
end
28+
end

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb

+22-6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
require "active_record/connection_adapters/oracle_enhanced/schema_definitions"
4242
require "active_record/connection_adapters/oracle_enhanced/schema_dumper"
4343
require "active_record/connection_adapters/oracle_enhanced/schema_statements"
44+
require "active_record/connection_adapters/oracle_enhanced/schema_statements_ext"
4445
require "active_record/connection_adapters/oracle_enhanced/context_index"
4546
require "active_record/connection_adapters/oracle_enhanced/column"
4647
require "active_record/connection_adapters/oracle_enhanced/quoting"
@@ -149,6 +150,7 @@ module ConnectionAdapters #:nodoc:
149150
class OracleEnhancedAdapter < AbstractAdapter
150151
include OracleEnhanced::DatabaseStatements
151152
include OracleEnhanced::SchemaStatements
153+
include OracleEnhanced::SchemaStatementsExt
152154
include OracleEnhanced::ContextIndex
153155
include OracleEnhanced::Quoting
154156
include OracleEnhanced::DatabaseLimits
@@ -477,7 +479,7 @@ def discard!
477479
# when inserting a new database record (see #prefetch_primary_key?).
478480
def next_sequence_value(sequence_name)
479481
# if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger
480-
raise ArgumentError.new "Trigger based primary key is not supported" if sequence_name == AUTOGENERATED_SEQUENCE_NAME
482+
return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME
481483
# call directly connection method to avoid prepared statement which causes fetching of next sequence value twice
482484
select_value(<<~SQL.squish, "SCHEMA")
483485
SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual
@@ -489,11 +491,8 @@ def next_sequence_value(sequence_name)
489491
def prefetch_primary_key?(table_name = nil)
490492
return true if table_name.nil?
491493
table_name = table_name.to_s
492-
do_not_prefetch = @do_not_prefetch_primary_key[table_name]
493-
if do_not_prefetch.nil?
494-
owner, desc_table_name = @connection.describe(table_name)
495-
@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name)
496-
end
494+
owner, desc_table_name = @connection.describe(table_name)
495+
do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name) || has_primary_key_trigger?(table_name, owner, desc_table_name)
497496
!do_not_prefetch
498497
end
499498

@@ -559,6 +558,23 @@ def default_tablespace
559558
SQL
560559
end
561560

561+
# check if table has primary key trigger with _pkt suffix
562+
def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil)
563+
(owner, desc_table_name) = @connection.describe(table_name) unless owner
564+
565+
trigger_name = default_trigger_name(table_name).upcase
566+
567+
!!select_value(<<-SQL.strip.gsub(/\s+/, " "), "Primary Key Trigger", [bind_string("owner", owner), bind_string("trigger_name", trigger_name), bind_string("owner", owner), bind_string("table_name", desc_table_name)])
568+
SELECT trigger_name
569+
FROM all_triggers
570+
WHERE owner = :owner
571+
AND trigger_name = :trigger_name
572+
AND table_owner = :owner
573+
AND table_name = :table_name
574+
AND status = 'ENABLED'
575+
SQL
576+
end
577+
562578
def column_definitions(table_name)
563579
(owner, desc_table_name) = @connection.describe(table_name)
564580

spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb

+18
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ def drop_test_posts_table
108108
end
109109
end
110110

111+
describe "table with primary key trigger" do
112+
113+
after(:each) do
114+
drop_test_posts_table
115+
end
116+
117+
it "should include primary key trigger in schema dump" do
118+
create_test_posts_table(primary_key_trigger: true)
119+
expect(standard_dump).to match(/create_table "test_posts".*add_primary_key_trigger "test_posts"/m)
120+
end
121+
122+
it "should include primary key trigger with non-default primary key in schema dump" do
123+
create_test_posts_table(primary_key_trigger: true, primary_key: "post_id")
124+
expect(standard_dump).to match(/create_table "test_posts", primary_key: "post_id".*add_primary_key_trigger "test_posts", primary_key: "post_id"/m)
125+
end
126+
127+
end
128+
111129
describe "foreign key constraints" do
112130
before(:all) do
113131
schema_define do

spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb

+204
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,172 @@ class ::TestEmployee < ActiveRecord::Base; end
152152
end
153153
end
154154

155+
describe "create table with primary key trigger" do
156+
def create_table_with_trigger(options = {})
157+
options.merge! primary_key_trigger: true, force: true
158+
schema_define do
159+
create_table :test_employees, options do |t|
160+
t.string :first_name
161+
t.string :last_name
162+
end
163+
end
164+
end
165+
166+
def create_table_and_separately_trigger(options = {})
167+
options.merge! force: true
168+
schema_define do
169+
create_table :test_employees, options do |t|
170+
t.string :first_name
171+
t.string :last_name
172+
end
173+
add_primary_key_trigger :test_employees, options
174+
end
175+
end
176+
177+
def drop_table_with_trigger(options = {})
178+
seq_name = options[:sequence_name]
179+
schema_define do
180+
drop_table :test_employees, (seq_name ? { sequence_name: seq_name } : {})
181+
end
182+
Object.send(:remove_const, "TestEmployee")
183+
ActiveRecord::Base.clear_cache!
184+
end
185+
186+
describe "with default primary key" do
187+
before(:all) do
188+
@conn = ActiveRecord::Base.connection
189+
create_table_with_trigger
190+
class ::TestEmployee < ActiveRecord::Base
191+
end
192+
end
193+
194+
after(:all) do
195+
drop_table_with_trigger
196+
end
197+
198+
it "should populate primary key using trigger" do
199+
expect do
200+
@conn.execute "INSERT INTO test_employees (first_name) VALUES ('Raimonds')"
201+
end.not_to raise_error
202+
end
203+
204+
it "should return new key value using connection insert method" do
205+
insert_id = @conn.insert("INSERT INTO test_employees (first_name) VALUES ('Raimonds')", nil, "id")
206+
expect(@conn.select_value("SELECT test_employees_seq.currval FROM dual")).to eq(insert_id)
207+
end
208+
209+
it "should create new record for model" do
210+
e = TestEmployee.create!(first_name: "Raimonds")
211+
expect(@conn.select_value("SELECT test_employees_seq.currval FROM dual")).to eq(e.id)
212+
end
213+
214+
it "should not generate NoMethodError for :returning_id:Symbol" do
215+
set_logger
216+
@conn.reconnect! unless @conn.active?
217+
@conn.insert("INSERT INTO test_employees (first_name) VALUES ('Yasuo')", nil, "id")
218+
expect(@logger.output(:error)).not_to match(/^Could not log "sql.active_record" event. NoMethodError: undefined method `name' for :returning_id:Symbol/)
219+
clear_logger
220+
end
221+
222+
end
223+
224+
describe "with separate creation of primary key trigger" do
225+
before(:all) do
226+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
227+
@conn = ActiveRecord::Base.connection
228+
create_table_and_separately_trigger
229+
class ::TestEmployee < ActiveRecord::Base
230+
end
231+
end
232+
233+
after(:all) do
234+
drop_table_with_trigger
235+
end
236+
237+
it "should populate primary key using trigger" do
238+
expect do
239+
@conn.execute "INSERT INTO test_employees (first_name) VALUES ('Raimonds')"
240+
end.not_to raise_error
241+
end
242+
243+
it "should return new key value using connection insert method" do
244+
insert_id = @conn.insert("INSERT INTO test_employees (first_name) VALUES ('Raimonds')", nil, "id")
245+
expect(@conn.select_value("SELECT test_employees_seq.currval FROM dual")).to eq(insert_id)
246+
end
247+
248+
it "should create new record for model" do
249+
e = TestEmployee.create!(first_name: "Raimonds")
250+
expect(@conn.select_value("SELECT test_employees_seq.currval FROM dual")).to eq(e.id)
251+
end
252+
end
253+
254+
describe "with non-default primary key and non-default sequence name" do
255+
before(:all) do
256+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
257+
@conn = ActiveRecord::Base.connection
258+
@primary_key = "employee_id"
259+
@sequence_name = "test_employees_s"
260+
create_table_with_trigger(primary_key: @primary_key, sequence_name: @sequence_name)
261+
class ::TestEmployee < ActiveRecord::Base
262+
self.primary_key = "employee_id"
263+
end
264+
end
265+
266+
after(:all) do
267+
drop_table_with_trigger(sequence_name: @sequence_name)
268+
end
269+
270+
it "should populate primary key using trigger" do
271+
expect do
272+
@conn.execute "INSERT INTO test_employees (first_name) VALUES ('Raimonds')"
273+
end.not_to raise_error
274+
end
275+
276+
it "should return new key value using connection insert method" do
277+
insert_id = @conn.insert("INSERT INTO test_employees (first_name) VALUES ('Raimonds')", nil, @primary_key)
278+
expect(@conn.select_value("SELECT #{@sequence_name}.currval FROM dual")).to eq(insert_id)
279+
end
280+
281+
it "should create new record for model with autogenerated sequence option" do
282+
e = TestEmployee.create!(first_name: "Raimonds")
283+
expect(@conn.select_value("SELECT #{@sequence_name}.currval FROM dual")).to eq(e.id)
284+
end
285+
end
286+
287+
describe "with non-default sequence name and non-default trigger name" do
288+
before(:all) do
289+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
290+
@conn = ActiveRecord::Base.connection
291+
@sequence_name = "test_employees_s"
292+
create_table_with_trigger(sequence_name: @sequence_name, trigger_name: "test_employees_t1")
293+
class ::TestEmployee < ActiveRecord::Base
294+
self.sequence_name = :autogenerated
295+
end
296+
end
297+
298+
after(:all) do
299+
drop_table_with_trigger(sequence_name: @sequence_name)
300+
end
301+
302+
it "should populate primary key using trigger" do
303+
expect do
304+
@conn.execute "INSERT INTO test_employees (first_name) VALUES ('Raimonds')"
305+
end.not_to raise_error
306+
end
307+
308+
it "should return new key value using connection insert method" do
309+
insert_id = @conn.insert("INSERT INTO test_employees (first_name) VALUES ('Raimonds')", nil, "id")
310+
expect(@conn.select_value("SELECT #{@sequence_name}.currval FROM dual")).to eq(insert_id)
311+
end
312+
313+
it "should create new record for model with autogenerated sequence option" do
314+
e = TestEmployee.create!(first_name: "Raimonds")
315+
expect(@conn.select_value("SELECT #{@sequence_name}.currval FROM dual")).to eq(e.id)
316+
end
317+
end
318+
319+
end
320+
155321
describe "table and column comments" do
156322
def create_test_employees_table(table_comment = nil, column_comments = {})
157323
schema_define do
@@ -302,6 +468,44 @@ class ::TestEmployee < ActiveRecord::Base; end
302468
end
303469
end
304470

471+
describe "create triggers" do
472+
473+
before(:all) do
474+
@conn = ActiveRecord::Base.connection
475+
schema_define do
476+
create_table :test_employees do |t|
477+
t.string :first_name
478+
t.string :last_name
479+
end
480+
end
481+
class ::TestEmployee < ActiveRecord::Base; end
482+
end
483+
484+
after(:all) do
485+
schema_define do
486+
drop_table :test_employees
487+
end
488+
Object.send(:remove_const, "TestEmployee")
489+
ActiveRecord::Base.clear_cache!
490+
end
491+
492+
it "should create table trigger with :new reference" do
493+
expect do
494+
@conn.execute <<-SQL
495+
CREATE OR REPLACE TRIGGER test_employees_pkt
496+
BEFORE INSERT ON test_employees FOR EACH ROW
497+
BEGIN
498+
IF inserting THEN
499+
IF :new.id IS NULL THEN
500+
SELECT test_employees_seq.NEXTVAL INTO :new.id FROM dual;
501+
END IF;
502+
END IF;
503+
END;
504+
SQL
505+
end.not_to raise_error
506+
end
507+
end
508+
305509
describe "add index" do
306510
before(:all) do
307511
@conn = ActiveRecord::Base.connection

0 commit comments

Comments
 (0)