Skip to content

Commit e605c2a

Browse files
committed
Use longer identifier for DatabaseLimits when using Oracle 12.2 or higher
Follow #1703. Oracle enhanced adapter supports longer identifier by #1703. I encountered the following error when using `rename_table` in Oracle 12c. ```console New table name 'identifier_of_thirty_bytes_or_more' is too long; the limit is 30 characters ``` Because `IDENTIFIER_MAX_LENGTH` constant was fixed to 30 bytes. This PR will use `max_identifier_length` method instead of the constant and accept longer identifier (max 128 bytes) when using Oracle 12.2 or higher. I encountered the error in Rails 6.0. So I'd like to backport to 6.1 and 6.0 stable branches.
1 parent 000bfb4 commit e605c2a

File tree

10 files changed

+86
-61
lines changed

10 files changed

+86
-61
lines changed

lib/active_record/connection_adapters/oracle_enhanced/connection.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ def self.create(config)
2020

2121
private
2222
# Used always by JDBC connection as well by OCI connection when describing tables over database link
23-
def describe(name)
23+
def describe(name, supports_longer_identifier)
2424
name = name.to_s
2525
if name.include?("@")
2626
raise ArgumentError "db link is not supported"
2727
else
2828
default_owner = @owner
2929
end
30-
real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
30+
real_name = OracleEnhanced::Quoting.valid_table_name?(name, supports_longer_identifier) ? name.upcase : name
3131
if real_name.include?(".")
3232
table_owner, table_name = real_name.split(".")
3333
else
@@ -57,7 +57,7 @@ def describe(name)
5757
if result = _select_one(sql, "CONNECTION", [table_owner, table_name, table_owner, table_name, table_owner, table_name, real_name])
5858
case result["name_type"]
5959
when "SYNONYM"
60-
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
60+
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}", supports_longer_identifier)
6161
else
6262
[result["owner"], result["table_name"]]
6363
end

lib/active_record/connection_adapters/oracle_enhanced/database_limits.rb

+5-8
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,31 @@ module ActiveRecord
66
module ConnectionAdapters
77
module OracleEnhanced
88
module DatabaseLimits
9-
# maximum length of Oracle identifiers
10-
IDENTIFIER_MAX_LENGTH = 30
11-
129
def table_alias_length #:nodoc:
13-
IDENTIFIER_MAX_LENGTH
10+
max_identifier_length
1411
end
1512

1613
# the maximum length of a table name
1714
def table_name_length
18-
IDENTIFIER_MAX_LENGTH
15+
max_identifier_length
1916
end
2017
deprecate :table_name_length
2118

2219
# the maximum length of a column name
2320
def column_name_length
24-
IDENTIFIER_MAX_LENGTH
21+
max_identifier_length
2522
end
2623
deprecate :column_name_length
2724

2825
# the maximum length of an index name
2926
# supported by this database
3027
def index_name_length
31-
IDENTIFIER_MAX_LENGTH
28+
max_identifier_length
3229
end
3330

3431
# the maximum length of a sequence name
3532
def sequence_name_length
36-
IDENTIFIER_MAX_LENGTH
33+
max_identifier_length
3734
end
3835

3936
# To avoid ORA-01795: maximum number of expressions in a list is 1000

lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def write_lob(lob, value, is_binary = false)
457457
end
458458

459459
# To allow private method called from `JDBCConnection`
460-
def describe(name)
460+
def describe(name, supports_longer_identifier)
461461
super
462462
end
463463

lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def write_lob(lob, value, is_binary = false)
225225
lob.write value
226226
end
227227

228-
def describe(name)
228+
def describe(name, supports_longer_identifier)
229229
super
230230
end
231231

lib/active_record/connection_adapters/oracle_enhanced/quoting.rb

+13-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def quote_column_name_or_expression(name) #:nodoc:
3838
end
3939
end
4040

41-
# Names must be from 1 to 30 bytes long with these exceptions:
41+
# Names must be from 1 to 30 bytes (Oracle 12.1 and lower) or from 1 to 128 bytes
42+
# (Oracle 12.2 and higher) long with these exceptions:
4243
# * Names of databases are limited to 8 bytes.
4344
# * Names of database links can be as long as 128 bytes.
4445
#
@@ -51,16 +52,23 @@ def quote_column_name_or_expression(name) #:nodoc:
5152
# your database character set and the underscore (_), dollar sign ($),
5253
# and pound sign (#).
5354
# Oracle strongly discourages you from using $ and # in nonquoted identifiers.
54-
NONQUOTED_OBJECT_NAME = /[[:alpha:]][\w$#]{0,29}/
55-
VALID_TABLE_NAME = /\A(?:#{NONQUOTED_OBJECT_NAME}\.)?#{NONQUOTED_OBJECT_NAME}?\Z/
55+
def self.nonquoted_object_name(supports_longer_identifier)
56+
@@nonquoted_object_name ||= supports_longer_identifier ? /[[:alpha:]][\w$#]{0,127}/ : /[[:alpha:]][\w$#]{0,29}/
57+
end
58+
59+
def self.valid_table_name(supports_longer_identifier)
60+
nonquoted_object_name = nonquoted_object_name(supports_longer_identifier)
61+
62+
@@valid_table_name ||= /\A(?:#{nonquoted_object_name}\.)?#{nonquoted_object_name}?\z/
63+
end
5664

5765
# unescaped table name should start with letter and
5866
# contain letters, digits, _, $ or #
5967
# can be prefixed with schema name
6068
# CamelCase table names should be quoted
61-
def self.valid_table_name?(name) #:nodoc:
69+
def self.valid_table_name?(name, supports_longer_identifier) #:nodoc:
6270
object_name = name.to_s
63-
!!(object_name =~ VALID_TABLE_NAME && !mixed_case?(object_name))
71+
!!(object_name =~ valid_table_name(supports_longer_identifier) && !mixed_case?(object_name))
6472
end
6573

6674
def self.mixed_case?(name)

lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def table_exists?(table_name)
3636
else
3737
default_owner = current_schema
3838
end
39-
real_name = OracleEnhanced::Quoting.valid_table_name?(table_name) ?
39+
real_name = OracleEnhanced::Quoting.valid_table_name?(table_name, supports_longer_identifier?) ?
4040
table_name.upcase : table_name
4141
if real_name.include?(".")
4242
table_owner, table_name = real_name.split(".")
@@ -53,7 +53,7 @@ def table_exists?(table_name)
5353
end
5454

5555
def data_source_exists?(table_name)
56-
(_owner, _table_name) = @connection.describe(table_name)
56+
(_owner, _table_name) = @connection.describe(table_name, supports_longer_identifier?)
5757
true
5858
rescue
5959
false
@@ -87,7 +87,7 @@ def synonyms
8787
end
8888

8989
def indexes(table_name) #:nodoc:
90-
(_owner, table_name) = @connection.describe(table_name)
90+
(_owner, table_name) = @connection.describe(table_name, supports_longer_identifier?)
9191
default_tablespace_name = default_tablespace
9292

9393
result = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
@@ -253,8 +253,8 @@ def primary_key(*args)
253253
end
254254

255255
def rename_table(table_name, new_name) #:nodoc:
256-
if new_name.to_s.length > DatabaseLimits::IDENTIFIER_MAX_LENGTH
257-
raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{DatabaseLimits::IDENTIFIER_MAX_LENGTH} characters"
256+
if new_name.to_s.length > max_identifier_length
257+
raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{max_identifier_length} characters"
258258
end
259259
schema_cache.clear_data_source_cache!(table_name.to_s)
260260
schema_cache.clear_data_source_cache!(new_name.to_s)
@@ -366,7 +366,7 @@ def index_name(table_name, options) #:nodoc:
366366
#
367367
# Will always query database and not index cache.
368368
def index_name_exists?(table_name, index_name)
369-
(_owner, table_name) = @connection.describe(table_name)
369+
(_owner, table_name) = @connection.describe(table_name, supports_longer_identifier?)
370370
result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
371371
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ 1 FROM all_indexes i
372372
WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -501,7 +501,7 @@ def change_column_comment(table_name, column_name, comment_or_changes)
501501

502502
def table_comment(table_name) #:nodoc:
503503
# TODO
504-
(_owner, table_name) = @connection.describe(table_name)
504+
(_owner, table_name) = @connection.describe(table_name, supports_longer_identifier?)
505505
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
506506
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ comments FROM all_tab_comments
507507
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -517,7 +517,7 @@ def table_options(table_name) # :nodoc:
517517

518518
def column_comment(table_name, column_name) #:nodoc:
519519
# TODO: it does not exist in Abstract adapter
520-
(_owner, table_name) = @connection.describe(table_name)
520+
(_owner, table_name) = @connection.describe(table_name, supports_longer_identifier?)
521521
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("column_name", column_name.upcase)])
522522
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ comments FROM all_col_comments
523523
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -545,7 +545,7 @@ def tablespace(table_name)
545545

546546
# get table foreign keys for schema dump
547547
def foreign_keys(table_name) #:nodoc:
548-
(_owner, desc_table_name) = @connection.describe(table_name)
548+
(_owner, desc_table_name) = @connection.describe(table_name, supports_longer_identifier?)
549549

550550
fk_info = select_all(<<~SQL.squish, "SCHEMA", [bind_string("desc_table_name", desc_table_name)])
551551
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ r.table_name to_table

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ def prefetch_primary_key?(table_name = nil)
491491
table_name = table_name.to_s
492492
do_not_prefetch = @do_not_prefetch_primary_key[table_name]
493493
if do_not_prefetch.nil?
494-
owner, desc_table_name = @connection.describe(table_name)
494+
owner, desc_table_name = @connection.describe(table_name, supports_longer_identifier?)
495495
@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name)
496496
end
497497
!do_not_prefetch
@@ -560,7 +560,7 @@ def default_tablespace
560560
end
561561

562562
def column_definitions(table_name)
563-
(owner, desc_table_name) = @connection.describe(table_name)
563+
(owner, desc_table_name) = @connection.describe(table_name, supports_longer_identifier?)
564564

565565
select_all(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("table_name", desc_table_name)])
566566
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ cols.column_name AS name, cols.data_type AS sql_type,
@@ -592,7 +592,7 @@ def clear_table_columns_cache(table_name)
592592
# Find a table's primary key and sequence.
593593
# *Note*: Only primary key is implemented - sequence will be nil.
594594
def pk_and_sequence_for(table_name, owner = nil, desc_table_name = nil) #:nodoc:
595-
(owner, desc_table_name) = @connection.describe(table_name)
595+
(owner, desc_table_name) = @connection.describe(table_name, supports_longer_identifier?)
596596

597597
seqs = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("sequence_name", default_sequence_name(desc_table_name))])
598598
select /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ us.sequence_name
@@ -634,7 +634,7 @@ def has_primary_key?(table_name, owner = nil, desc_table_name = nil) #:nodoc:
634634
end
635635

636636
def primary_keys(table_name) # :nodoc:
637-
(_owner, desc_table_name) = @connection.describe(table_name)
637+
(_owner, desc_table_name) = @connection.describe(table_name, supports_longer_identifier?)
638638

639639
pks = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("table_name", desc_table_name)])
640640
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ cc.column_name

spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb

+12-9
Original file line numberDiff line numberDiff line change
@@ -495,53 +495,56 @@ def kill_current_session
495495
before(:all) do
496496
@conn = ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection.create(CONNECTION_PARAMS)
497497
@owner = CONNECTION_PARAMS[:username].upcase
498+
499+
@oracle12cr2_or_higher = !!@conn.exec(
500+
"select * from product_component_version where product like 'Oracle%' and to_number(substr(version,1,4)) >= 12.2").fetch
498501
end
499502

500503
it "should describe existing table" do
501504
@conn.exec "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
502-
expect(@conn.describe("test_employees")).to eq([@owner, "TEST_EMPLOYEES"])
505+
expect(@conn.describe("test_employees", @oracle12cr2_or_higher)).to eq([@owner, "TEST_EMPLOYEES"])
503506
@conn.exec "DROP TABLE test_employees" rescue nil
504507
end
505508

506509
it "should not describe non-existing table" do
507-
expect { @conn.describe("test_xxx") }.to raise_error(ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException)
510+
expect { @conn.describe("test_xxx", @oracle12cr2_or_higher) }.to raise_error(ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException)
508511
end
509512

510513
it "should describe table in other schema" do
511-
expect(@conn.describe("sys.dual")).to eq(["SYS", "DUAL"])
514+
expect(@conn.describe("sys.dual", @oracle12cr2_or_higher)).to eq(["SYS", "DUAL"])
512515
end
513516

514517
it "should describe table in other schema if the schema and table are in different cases" do
515-
expect(@conn.describe("SYS.dual")).to eq(["SYS", "DUAL"])
518+
expect(@conn.describe("SYS.dual", @oracle12cr2_or_higher)).to eq(["SYS", "DUAL"])
516519
end
517520

518521
it "should describe existing view" do
519522
@conn.exec "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
520523
@conn.exec "CREATE VIEW test_employees_v AS SELECT * FROM test_employees" rescue nil
521-
expect(@conn.describe("test_employees_v")).to eq([@owner, "TEST_EMPLOYEES_V"])
524+
expect(@conn.describe("test_employees_v", @oracle12cr2_or_higher)).to eq([@owner, "TEST_EMPLOYEES_V"])
522525
@conn.exec "DROP VIEW test_employees_v" rescue nil
523526
@conn.exec "DROP TABLE test_employees" rescue nil
524527
end
525528

526529
it "should describe view in other schema" do
527-
expect(@conn.describe("sys.v_$version")).to eq(["SYS", "V_$VERSION"])
530+
expect(@conn.describe("sys.v_$version", @oracle12cr2_or_higher)).to eq(["SYS", "V_$VERSION"])
528531
end
529532

530533
it "should describe existing private synonym" do
531534
@conn.exec "CREATE SYNONYM test_dual FOR sys.dual" rescue nil
532-
expect(@conn.describe("test_dual")).to eq(["SYS", "DUAL"])
535+
expect(@conn.describe("test_dual", @oracle12cr2_or_higher)).to eq(["SYS", "DUAL"])
533536
@conn.exec "DROP SYNONYM test_dual" rescue nil
534537
end
535538

536539
it "should describe existing public synonym" do
537-
expect(@conn.describe("all_tables")).to eq(["SYS", "ALL_TABLES"])
540+
expect(@conn.describe("all_tables", @oracle12cr2_or_higher)).to eq(["SYS", "ALL_TABLES"])
538541
end
539542

540543
if defined?(OCI8)
541544
context "OCI8 adapter" do
542545
it "should not fallback to SELECT-based logic when querying non-existent table information" do
543546
expect(@conn).not_to receive(:select_one)
544-
@conn.describe("non_existent") rescue ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException
547+
@conn.describe("non_existent", @oracle12cr2_or_higher) rescue ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException
545548
end
546549
end
547550
end

spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb

+29-18
Original file line numberDiff line numberDiff line change
@@ -64,55 +64,66 @@ class ::TestReservedWord < ActiveRecord::Base; end
6464
describe "valid table names" do
6565
before(:all) do
6666
@adapter = ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting
67+
68+
@oracle12cr2_or_higher = !!ActiveRecord::Base.connection.select_value(
69+
"select * from product_component_version where product like 'Oracle%' and to_number(substr(version,1,4)) >= 12.2")
6770
end
6871

6972
it "should be valid with letters and digits" do
70-
expect(@adapter.valid_table_name?("abc_123")).to be_truthy
73+
expect(@adapter.valid_table_name?("abc_123", @oracle12cr2_or_higher)).to be_truthy
7174
end
7275

7376
it "should be valid with schema name" do
74-
expect(@adapter.valid_table_name?("abc_123.def_456")).to be_truthy
77+
expect(@adapter.valid_table_name?("abc_123.def_456", @oracle12cr2_or_higher)).to be_truthy
7578
end
7679

7780
it "should be valid with schema name and object name in different case" do
78-
expect(@adapter.valid_table_name?("TEST_DBA.def_456")).to be_truthy
81+
expect(@adapter.valid_table_name?("TEST_DBA.def_456", @oracle12cr2_or_higher)).to be_truthy
7982
end
8083

8184
it "should be valid with $ in name" do
82-
expect(@adapter.valid_table_name?("sys.v$session")).to be_truthy
85+
expect(@adapter.valid_table_name?("sys.v$session", @oracle12cr2_or_higher)).to be_truthy
8386
end
8487

8588
it "should be valid with upcase schema name" do
86-
expect(@adapter.valid_table_name?("ABC_123.DEF_456")).to be_truthy
89+
expect(@adapter.valid_table_name?("ABC_123.DEF_456", @oracle12cr2_or_higher)).to be_truthy
8790
end
8891

8992
it "should not be valid with two dots in name" do
90-
expect(@adapter.valid_table_name?("abc_123.def_456.ghi_789")).to be_falsey
93+
expect(@adapter.valid_table_name?("abc_123.def_456.ghi_789", @oracle12cr2_or_higher)).to be_falsey
9194
end
9295

9396
it "should not be valid with invalid characters" do
94-
expect(@adapter.valid_table_name?("warehouse-things")).to be_falsey
97+
expect(@adapter.valid_table_name?("warehouse-things", @oracle12cr2_or_higher)).to be_falsey
9598
end
9699

97100
it "should not be valid with for camel-case" do
98-
expect(@adapter.valid_table_name?("Abc")).to be_falsey
99-
expect(@adapter.valid_table_name?("aBc")).to be_falsey
100-
expect(@adapter.valid_table_name?("abC")).to be_falsey
101+
expect(@adapter.valid_table_name?("Abc", @oracle12cr2_or_higher)).to be_falsey
102+
expect(@adapter.valid_table_name?("aBc", @oracle12cr2_or_higher)).to be_falsey
103+
expect(@adapter.valid_table_name?("abC", @oracle12cr2_or_higher)).to be_falsey
101104
end
102105

103-
it "should not be valid for names > 30 characters" do
104-
expect(@adapter.valid_table_name?("a" * 31)).to be_falsey
106+
it "should not be valid for names over maximum characters" do
107+
if @oracle12cr2_or_higher
108+
expect(@adapter.valid_table_name?("a" * 129, @oracle12cr2_or_higher)).to be_falsey
109+
else
110+
expect(@adapter.valid_table_name?("a" * 31, @oracle12cr2_or_higher)).to be_falsey
111+
end
105112
end
106113

107-
it "should not be valid for schema names > 30 characters" do
108-
expect(@adapter.valid_table_name?(("a" * 31) + ".validname")).to be_falsey
114+
it "should not be valid for schema names over maximum characters" do
115+
if @oracle12cr2_or_higher
116+
expect(@adapter.valid_table_name?(("a" * 129) + ".validname", @oracle12cr2_or_higher)).to be_falsey
117+
else
118+
expect(@adapter.valid_table_name?(("a" * 31) + ".validname", @oracle12cr2_or_higher)).to be_falsey
119+
end
109120
end
110121

111122
it "should not be valid for names that do not begin with alphabetic characters" do
112-
expect(@adapter.valid_table_name?("1abc")).to be_falsey
113-
expect(@adapter.valid_table_name?("_abc")).to be_falsey
114-
expect(@adapter.valid_table_name?("abc.1xyz")).to be_falsey
115-
expect(@adapter.valid_table_name?("abc._xyz")).to be_falsey
123+
expect(@adapter.valid_table_name?("1abc", @oracle12cr2_or_higher)).to be_falsey
124+
expect(@adapter.valid_table_name?("_abc", @oracle12cr2_or_higher)).to be_falsey
125+
expect(@adapter.valid_table_name?("abc.1xyz", @oracle12cr2_or_higher)).to be_falsey
126+
expect(@adapter.valid_table_name?("abc._xyz", @oracle12cr2_or_higher)).to be_falsey
116127
end
117128
end
118129

0 commit comments

Comments
 (0)