activerecord-oracle_enhanced-adapter 8.1.2 → 8.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +1 -84
- data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +0 -5
- data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +0 -4
- data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +81 -7
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +7 -6
- data/lib/arel/visitors/oracle.rb +6 -2
- data/lib/arel/visitors/oracle_common.rb +29 -0
- data/spec/active_record/connection_adapters/oracle_enhanced/columns_for_distinct_spec.rb +94 -0
- data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +150 -32
- data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +22 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +77 -1
- data/spec/support/create_oracle_enhanced_users.sql +4 -20
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f69a5874a83fcb0afd6b60ae09f61e0a300201cf3a99b6aee5523dcd92a985b
|
|
4
|
+
data.tar.gz: 8eee0fdb3853c42f27d37b37726ea9c4d685cadc047d00971291b0efe36503bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8f684da4497ca2b4534744100d2c1468a7871420b127d4996816057e90d62213e5f456816c1e294920609f6f01122c778c0ff76b7479e3fa52b8f19c4694728
|
|
7
|
+
data.tar.gz: a2d729ffbcf21518d3b5a291e913c79104308d191e2ee6a54ac60b18418f4c8732e3069da2ecd5302cc6dd9f10e43d35c043fbeba204d99cb0b6e3b527cc0479
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.1.
|
|
1
|
+
8.1.4
|
|
@@ -16,102 +16,19 @@ module ActiveRecord
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
attr_reader :raw_connection
|
|
19
|
+
attr_reader :raw_connection, :owner
|
|
20
20
|
|
|
21
21
|
private
|
|
22
|
-
# Used always by JDBC connection as well by OCI connection when describing tables over database link
|
|
23
|
-
def describe(name)
|
|
24
|
-
name = name.to_s
|
|
25
|
-
if name.include?("@")
|
|
26
|
-
raise ArgumentError "db link is not supported"
|
|
27
|
-
else
|
|
28
|
-
default_owner = @owner
|
|
29
|
-
end
|
|
30
|
-
real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
|
|
31
|
-
if real_name.include?(".")
|
|
32
|
-
table_owner, table_name = real_name.split(".")
|
|
33
|
-
else
|
|
34
|
-
table_owner, table_name = default_owner, real_name
|
|
35
|
-
end
|
|
36
|
-
sql = <<~SQL.squish
|
|
37
|
-
SELECT owner, table_name, 'TABLE' name_type
|
|
38
|
-
FROM all_tables
|
|
39
|
-
WHERE owner = :table_owner
|
|
40
|
-
AND table_name = :table_name
|
|
41
|
-
UNION ALL
|
|
42
|
-
SELECT owner, view_name table_name, 'VIEW' name_type
|
|
43
|
-
FROM all_views
|
|
44
|
-
WHERE owner = :table_owner
|
|
45
|
-
AND view_name = :table_name
|
|
46
|
-
UNION ALL
|
|
47
|
-
SELECT table_owner, table_name, 'SYNONYM' name_type
|
|
48
|
-
FROM all_synonyms
|
|
49
|
-
WHERE owner = :table_owner
|
|
50
|
-
AND synonym_name = :table_name
|
|
51
|
-
UNION ALL
|
|
52
|
-
SELECT table_owner, table_name, 'SYNONYM' name_type
|
|
53
|
-
FROM all_synonyms
|
|
54
|
-
WHERE owner = 'PUBLIC'
|
|
55
|
-
AND synonym_name = :real_name
|
|
56
|
-
SQL
|
|
57
|
-
if result = _select_one(sql, "CONNECTION", [table_owner, table_name, table_owner, table_name, table_owner, table_name, real_name])
|
|
58
|
-
case result["name_type"]
|
|
59
|
-
when "SYNONYM"
|
|
60
|
-
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
|
|
61
|
-
else
|
|
62
|
-
[result["owner"], result["table_name"]]
|
|
63
|
-
end
|
|
64
|
-
else
|
|
65
|
-
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
22
|
# Oracle column names by default are case-insensitive, but treated as upcase;
|
|
70
23
|
# for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
|
|
71
24
|
# their column names when creating Oracle tables, which makes then case-sensitive.
|
|
72
25
|
# I don't know anybody who does this, but we'll handle the theoretical case of a
|
|
73
26
|
# camelCase column name. I imagine other dbs handle this different, since there's a
|
|
74
27
|
# unit test that's currently failing test_oci.
|
|
75
|
-
#
|
|
76
|
-
# `_oracle_downcase` is expected to be called only from
|
|
77
|
-
# `ActiveRecord::ConnectionAdapters::OracleEnhanced::OCIConnection`
|
|
78
|
-
# or `ActiveRecord::ConnectionAdapters::OracleEnhanced::JDBCConnection`.
|
|
79
|
-
# Other method should call `ActiveRecord:: ConnectionAdapters::OracleEnhanced::Quoting#oracle_downcase`
|
|
80
|
-
# since this is kind of quoting, not connection.
|
|
81
|
-
# To avoid it is called from anywhere else, added _ at the beginning of the method name.
|
|
82
28
|
def _oracle_downcase(column_name)
|
|
83
29
|
return nil if column_name.nil?
|
|
84
30
|
/[a-z]/.match?(column_name) ? column_name : column_name.downcase
|
|
85
31
|
end
|
|
86
|
-
|
|
87
|
-
# _select_one and _select_value methods are expected to be called
|
|
88
|
-
# only from `ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection#describe`
|
|
89
|
-
# Other methods should call `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_one`
|
|
90
|
-
# and `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_value`
|
|
91
|
-
# To avoid called from its subclass added a underscore in each method.
|
|
92
|
-
|
|
93
|
-
# Returns a record hash with the column names as keys and column values
|
|
94
|
-
# as values.
|
|
95
|
-
# binds is a array of native values in contrast to ActiveRecord::Relation::QueryAttribute
|
|
96
|
-
def _select_one(arel, name = nil, binds = [])
|
|
97
|
-
cursor = prepare(arel)
|
|
98
|
-
cursor.bind_params(binds)
|
|
99
|
-
cursor.exec
|
|
100
|
-
columns = cursor.get_col_names.map do |col_name|
|
|
101
|
-
_oracle_downcase(col_name)
|
|
102
|
-
end
|
|
103
|
-
row = cursor.fetch
|
|
104
|
-
columns.each_with_index.to_h { |x, i| [x, row[i]] } if row
|
|
105
|
-
ensure
|
|
106
|
-
cursor.close
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Returns a single value from a record
|
|
110
|
-
def _select_value(arel, name = nil, binds = [])
|
|
111
|
-
if result = _select_one(arel, name, binds)
|
|
112
|
-
result.values.first
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
32
|
end
|
|
116
33
|
|
|
117
34
|
# Returns array with major and minor version of database (e.g. [12, 1])
|
|
@@ -53,7 +53,7 @@ module ActiveRecord
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def data_source_exists?(table_name)
|
|
56
|
-
(_owner, _table_name) =
|
|
56
|
+
(_owner, _table_name) = resolve_data_source_name(table_name)
|
|
57
57
|
true
|
|
58
58
|
rescue
|
|
59
59
|
false
|
|
@@ -87,7 +87,7 @@ module ActiveRecord
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def indexes(table_name) # :nodoc:
|
|
90
|
-
(_owner, table_name) =
|
|
90
|
+
(_owner, table_name) = resolve_data_source_name(table_name)
|
|
91
91
|
default_tablespace_name = default_tablespace
|
|
92
92
|
|
|
93
93
|
result = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
|
|
@@ -259,7 +259,7 @@ module ActiveRecord
|
|
|
259
259
|
schema_cache.clear_data_source_cache!(table_name.to_s)
|
|
260
260
|
schema_cache.clear_data_source_cache!(new_name.to_s)
|
|
261
261
|
execute "RENAME #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
|
262
|
-
execute "RENAME #{
|
|
262
|
+
execute "RENAME #{default_sequence_name(table_name)} TO #{default_sequence_name(new_name)}" rescue nil
|
|
263
263
|
|
|
264
264
|
rename_table_indexes(table_name, new_name, **options)
|
|
265
265
|
end
|
|
@@ -368,7 +368,7 @@ module ActiveRecord
|
|
|
368
368
|
#
|
|
369
369
|
# Will always query database and not index cache.
|
|
370
370
|
def index_name_exists?(table_name, index_name)
|
|
371
|
-
(_owner, table_name) =
|
|
371
|
+
(_owner, table_name) = resolve_data_source_name(table_name)
|
|
372
372
|
result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
|
|
373
373
|
SELECT 1 FROM all_indexes i
|
|
374
374
|
WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
|
|
@@ -511,7 +511,7 @@ module ActiveRecord
|
|
|
511
511
|
|
|
512
512
|
def table_comment(table_name) # :nodoc:
|
|
513
513
|
# TODO
|
|
514
|
-
(_owner, table_name) =
|
|
514
|
+
(_owner, table_name) = resolve_data_source_name(table_name)
|
|
515
515
|
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
|
|
516
516
|
SELECT comments FROM all_tab_comments
|
|
517
517
|
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
|
|
@@ -527,7 +527,7 @@ module ActiveRecord
|
|
|
527
527
|
|
|
528
528
|
def column_comment(table_name, column_name) # :nodoc:
|
|
529
529
|
# TODO: it does not exist in Abstract adapter
|
|
530
|
-
(_owner, table_name) =
|
|
530
|
+
(_owner, table_name) = resolve_data_source_name(table_name)
|
|
531
531
|
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("column_name", column_name.upcase)])
|
|
532
532
|
SELECT comments FROM all_col_comments
|
|
533
533
|
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
|
|
@@ -555,7 +555,7 @@ module ActiveRecord
|
|
|
555
555
|
|
|
556
556
|
# get table foreign keys for schema dump
|
|
557
557
|
def foreign_keys(table_name) # :nodoc:
|
|
558
|
-
(_owner, desc_table_name) =
|
|
558
|
+
(_owner, desc_table_name) = resolve_data_source_name(table_name)
|
|
559
559
|
|
|
560
560
|
fk_info = select_all(<<~SQL.squish, "SCHEMA", [bind_string("desc_table_name", desc_table_name)])
|
|
561
561
|
SELECT r.table_name to_table
|
|
@@ -733,6 +733,80 @@ module ActiveRecord
|
|
|
733
733
|
|
|
734
734
|
execute("ALTER INDEX #{quote_column_name(index_name)} REBUILD TABLESPACE #{tablespace}")
|
|
735
735
|
end
|
|
736
|
+
|
|
737
|
+
# Resolves an Oracle data-source name to its underlying [owner, table_name]
|
|
738
|
+
# by following synonyms through the catalog. Defaults the schema to
|
|
739
|
+
# `_connection.owner` (the adapter's configured default schema, taken
|
|
740
|
+
# from `config[:schema]` or `config[:username]`) when the name is not
|
|
741
|
+
# schema-qualified. This is distinct from
|
|
742
|
+
# `SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')`, which can differ after
|
|
743
|
+
# `ALTER SESSION SET CURRENT_SCHEMA`.
|
|
744
|
+
# Raises OracleEnhanced::ConnectionException if the object does not
|
|
745
|
+
# exist or if synonym resolution produces a looping chain.
|
|
746
|
+
def resolve_data_source_name(name)
|
|
747
|
+
visited = Set.new
|
|
748
|
+
loop do
|
|
749
|
+
schema, identifier = extract_schema_qualified_name(name)
|
|
750
|
+
real_name = schema ? "#{schema}.#{identifier}" : identifier
|
|
751
|
+
owner = schema || _connection.owner
|
|
752
|
+
|
|
753
|
+
unless visited.add?([owner, identifier])
|
|
754
|
+
raise OracleEnhanced::ConnectionException,
|
|
755
|
+
%Q{"DESC #{name}" failed; looping chain of synonyms}
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
binds = [
|
|
759
|
+
bind_string("table_owner", owner),
|
|
760
|
+
bind_string("table_name", identifier),
|
|
761
|
+
bind_string("real_name", real_name),
|
|
762
|
+
]
|
|
763
|
+
# Single-pass lookup against all_objects, ordered so the first row
|
|
764
|
+
# is the one the legacy 4-way UNION ALL (all_tables, all_views,
|
|
765
|
+
# owner synonym, public synonym) would have returned: prefer a
|
|
766
|
+
# match in the caller's schema over PUBLIC, and within a schema
|
|
767
|
+
# prefer TABLE over VIEW over SYNONYM.
|
|
768
|
+
result = select_one(<<~SQL.squish, "SCHEMA", binds)
|
|
769
|
+
SELECT owner, object_name table_name, object_type name_type
|
|
770
|
+
FROM all_objects
|
|
771
|
+
WHERE ((owner = :table_owner AND object_name = :table_name)
|
|
772
|
+
OR (owner = 'PUBLIC' AND object_name = :real_name))
|
|
773
|
+
AND object_type IN ('TABLE', 'VIEW', 'SYNONYM')
|
|
774
|
+
ORDER BY DECODE(owner, 'PUBLIC', 2, 1),
|
|
775
|
+
DECODE(object_type, 'TABLE', 1, 'VIEW', 2, 'SYNONYM', 3)
|
|
776
|
+
SQL
|
|
777
|
+
|
|
778
|
+
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?} unless result
|
|
779
|
+
|
|
780
|
+
if result["name_type"] == "SYNONYM"
|
|
781
|
+
synonym_binds = [
|
|
782
|
+
bind_string("owner", result["owner"]),
|
|
783
|
+
bind_string("synonym_name", result["table_name"]),
|
|
784
|
+
]
|
|
785
|
+
syn = select_one(<<~SQL.squish, "SCHEMA", synonym_binds)
|
|
786
|
+
SELECT table_owner, table_name
|
|
787
|
+
FROM all_synonyms
|
|
788
|
+
WHERE owner = :owner AND synonym_name = :synonym_name
|
|
789
|
+
SQL
|
|
790
|
+
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?} unless syn
|
|
791
|
+
name = "#{syn['table_owner'] && "#{syn['table_owner']}."}#{syn['table_name']}"
|
|
792
|
+
else
|
|
793
|
+
return [result["owner"], result["table_name"]]
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# Splits "schema.identifier" into its parts, returning [schema, identifier].
|
|
799
|
+
# Mirrors Rails' PostgreSQL/MySQL adapters: a non-qualified name yields
|
|
800
|
+
# schema = nil. Oracle-specific bits: rejects db links and upcases valid
|
|
801
|
+
# identifiers so catalog lookups match the stored upper-case names.
|
|
802
|
+
def extract_schema_qualified_name(string)
|
|
803
|
+
string = string.to_s
|
|
804
|
+
raise ArgumentError, "db link is not supported" if string.include?("@")
|
|
805
|
+
|
|
806
|
+
string = string.upcase if OracleEnhanced::Quoting.valid_table_name?(string)
|
|
807
|
+
schema, identifier = string.split(".") if string.include?(".")
|
|
808
|
+
[schema, identifier || string]
|
|
809
|
+
end
|
|
736
810
|
end
|
|
737
811
|
end
|
|
738
812
|
end
|
|
@@ -519,7 +519,7 @@ module ActiveRecord
|
|
|
519
519
|
table_name = table_name.to_s
|
|
520
520
|
do_not_prefetch = @do_not_prefetch_primary_key[table_name]
|
|
521
521
|
if do_not_prefetch.nil?
|
|
522
|
-
owner, desc_table_name =
|
|
522
|
+
owner, desc_table_name = resolve_data_source_name(table_name)
|
|
523
523
|
@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name)
|
|
524
524
|
end
|
|
525
525
|
!do_not_prefetch
|
|
@@ -588,7 +588,7 @@ module ActiveRecord
|
|
|
588
588
|
end
|
|
589
589
|
|
|
590
590
|
def column_definitions(table_name)
|
|
591
|
-
(owner, desc_table_name) =
|
|
591
|
+
(owner, desc_table_name) = resolve_data_source_name(table_name)
|
|
592
592
|
|
|
593
593
|
select_all(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("table_name", desc_table_name)])
|
|
594
594
|
SELECT cols.column_name AS name, cols.data_type AS sql_type,
|
|
@@ -620,7 +620,7 @@ module ActiveRecord
|
|
|
620
620
|
# Find a table's primary key and sequence.
|
|
621
621
|
# *Note*: Only primary key is implemented - sequence will be nil.
|
|
622
622
|
def pk_and_sequence_for(table_name, owner = nil, desc_table_name = nil) # :nodoc:
|
|
623
|
-
(owner, desc_table_name) =
|
|
623
|
+
(owner, desc_table_name) = resolve_data_source_name(table_name)
|
|
624
624
|
|
|
625
625
|
seqs = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("sequence_name", default_sequence_name(desc_table_name))])
|
|
626
626
|
select us.sequence_name
|
|
@@ -662,7 +662,7 @@ module ActiveRecord
|
|
|
662
662
|
end
|
|
663
663
|
|
|
664
664
|
def primary_keys(table_name) # :nodoc:
|
|
665
|
-
(_owner, desc_table_name) =
|
|
665
|
+
(_owner, desc_table_name) = resolve_data_source_name(table_name)
|
|
666
666
|
|
|
667
667
|
pks = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("table_name", desc_table_name)])
|
|
668
668
|
SELECT cc.column_name
|
|
@@ -685,8 +685,9 @@ module ActiveRecord
|
|
|
685
685
|
# It does not construct DISTINCT clause. Just return column names for distinct.
|
|
686
686
|
order_columns = orders.reject(&:blank?).map { |s|
|
|
687
687
|
s = visitor.compile(s) unless s.is_a?(String)
|
|
688
|
-
# remove any ASC/DESC modifiers
|
|
689
|
-
s.gsub(/\s+(ASC|DESC)\
|
|
688
|
+
# remove any ASC/DESC and NULLS FIRST/LAST modifiers
|
|
689
|
+
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
|
|
690
|
+
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
|
|
690
691
|
}.reject(&:blank?).map.with_index { |column, i|
|
|
691
692
|
"FIRST_VALUE(#{column}) OVER (PARTITION BY #{columns.join(', ')} ORDER BY #{column}) AS alias_#{i}__"
|
|
692
693
|
}
|
data/lib/arel/visitors/oracle.rb
CHANGED
|
@@ -172,8 +172,12 @@ module Arel # :nodoc: all
|
|
|
172
172
|
end.flatten
|
|
173
173
|
o.orders = []
|
|
174
174
|
orders.each_with_index do |order, i|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
parts = ["alias_#{i}__"]
|
|
176
|
+
parts << "DESC" if /\bdesc\b/i.match?(order)
|
|
177
|
+
if (nulls_match = order.match(/\bNULLS\s+(FIRST|LAST)\b/i))
|
|
178
|
+
parts << "NULLS #{nulls_match[1].upcase}"
|
|
179
|
+
end
|
|
180
|
+
o.orders << Nodes::SqlLiteral.new(parts.join(" "))
|
|
177
181
|
end
|
|
178
182
|
o
|
|
179
183
|
end
|
|
@@ -46,6 +46,35 @@ module Arel # :nodoc: all
|
|
|
46
46
|
def schema_cache
|
|
47
47
|
@connection.schema_cache
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def visit_Arel_Nodes_In(o, collector)
|
|
51
|
+
attr, values = o.left, o.right
|
|
52
|
+
return super unless values.is_a?(Array)
|
|
53
|
+
|
|
54
|
+
in_clause_length = @connection.in_clause_length
|
|
55
|
+
return super if values.length <= in_clause_length
|
|
56
|
+
|
|
57
|
+
# Split into multiple IN nodes and combine with OR
|
|
58
|
+
in_nodes = values.each_slice(in_clause_length).map do |slice|
|
|
59
|
+
Arel::Nodes::In.new(attr, slice)
|
|
60
|
+
end
|
|
61
|
+
or_node = Arel::Nodes::Or.new(in_nodes)
|
|
62
|
+
visit(Arel::Nodes::Grouping.new(or_node), collector)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_Arel_Nodes_NotIn(o, collector)
|
|
66
|
+
attr, values = o.left, o.right
|
|
67
|
+
return super unless values.is_a?(Array)
|
|
68
|
+
|
|
69
|
+
in_clause_length = @connection.in_clause_length
|
|
70
|
+
return super if values.length <= in_clause_length
|
|
71
|
+
|
|
72
|
+
# Split into multiple NOT IN nodes and combine with AND
|
|
73
|
+
not_in_nodes = values.each_slice(in_clause_length).map do |slice|
|
|
74
|
+
Arel::Nodes::NotIn.new(attr, slice)
|
|
75
|
+
end
|
|
76
|
+
visit(Arel::Nodes::And.new(not_in_nodes), collector)
|
|
77
|
+
end
|
|
49
78
|
end
|
|
50
79
|
end
|
|
51
80
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
describe "OracleEnhancedAdapter#columns_for_distinct" do
|
|
4
|
+
before(:all) do
|
|
5
|
+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
|
|
6
|
+
@conn = ActiveRecord::Base.connection
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "strips ASC modifier" do
|
|
10
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at ASC"])
|
|
11
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
12
|
+
expect(sql).not_to match(/\bASC\b/i)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "strips DESC modifier" do
|
|
16
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at DESC"])
|
|
17
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
18
|
+
expect(sql).not_to match(/\bDESC\b/i)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "strips NULLS FIRST modifier" do
|
|
22
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at NULLS FIRST"])
|
|
23
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
24
|
+
expect(sql).not_to match(/NULLS\s+FIRST/i)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "strips NULLS LAST modifier" do
|
|
28
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at NULLS LAST"])
|
|
29
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
30
|
+
expect(sql).not_to match(/NULLS\s+LAST/i)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "strips combined DESC NULLS LAST" do
|
|
34
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at DESC NULLS LAST"])
|
|
35
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
36
|
+
expect(sql).not_to match(/\bDESC\b/i)
|
|
37
|
+
expect(sql).not_to match(/NULLS/i)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "strips combined ASC NULLS FIRST" do
|
|
41
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at ASC NULLS FIRST"])
|
|
42
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
43
|
+
expect(sql).not_to match(/\bASC\b/i)
|
|
44
|
+
expect(sql).not_to match(/NULLS/i)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "strips lowercase desc nulls last" do
|
|
48
|
+
sql = @conn.columns_for_distinct(["posts.id"], ["posts.created_at desc nulls last"])
|
|
49
|
+
expect(sql).to include("FIRST_VALUE(posts.created_at)")
|
|
50
|
+
expect(sql).not_to match(/\bdesc\b/i)
|
|
51
|
+
expect(sql).not_to match(/nulls/i)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "joins composite primary key columns in PARTITION BY" do
|
|
55
|
+
sql = @conn.columns_for_distinct(["posts.id", "posts.tenant_id"], ["posts.created_at DESC"])
|
|
56
|
+
expect(sql).to include("PARTITION BY posts.id, posts.tenant_id")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "strips DESC NULLS LAST from an Arel ordering node" do
|
|
60
|
+
order_node = Arel::Table.new(:posts)[:created_at].desc.nulls_last
|
|
61
|
+
sql = @conn.columns_for_distinct(["posts.id"], [order_node])
|
|
62
|
+
expect(sql).to match(/FIRST_VALUE\("POSTS"\."CREATED_AT"\)/i)
|
|
63
|
+
expect(sql).not_to match(/\bDESC\b/i)
|
|
64
|
+
expect(sql).not_to match(/NULLS/i)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "integration with Arel::Visitors::Oracle#order_hacks" do
|
|
68
|
+
def compile_distinct_order(distinct_columns, order)
|
|
69
|
+
projection = "DISTINCT #{distinct_columns.join(', ')}, " \
|
|
70
|
+
"#{@conn.columns_for_distinct(distinct_columns, [order])}"
|
|
71
|
+
stmt = Arel::Nodes::SelectStatement.new
|
|
72
|
+
stmt.cores.first.projections << Arel::Nodes::SqlLiteral.new(projection)
|
|
73
|
+
stmt.orders << (order.is_a?(String) ? Arel::Nodes::SqlLiteral.new(order) : order)
|
|
74
|
+
@conn.visitor.accept(stmt, Arel::Collectors::SQLString.new).value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "preserves DESC NULLS LAST in the outer ORDER BY for a raw SQL order" do
|
|
78
|
+
sql = compile_distinct_order(["posts.id"], "posts.created_at DESC NULLS LAST")
|
|
79
|
+
expect(sql).to include("ORDER BY alias_0__ DESC NULLS LAST")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "preserves DESC NULLS LAST in the outer ORDER BY for an Arel ordering node" do
|
|
83
|
+
order_node = Arel::Table.new(:posts)[:created_at].desc.nulls_last
|
|
84
|
+
sql = compile_distinct_order(["posts.id"], order_node)
|
|
85
|
+
expect(sql).to include("ORDER BY alias_0__ DESC NULLS LAST")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "preserves NULLS FIRST without emitting DESC for an ASC-with-nulls order" do
|
|
89
|
+
sql = compile_distinct_order(["posts.id"], "posts.created_at ASC NULLS FIRST")
|
|
90
|
+
expect(sql).to match(/ORDER BY alias_0__ NULLS FIRST\b/)
|
|
91
|
+
expect(sql).not_to match(/alias_0__ DESC/)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -531,59 +531,177 @@ describe "OracleEnhancedConnection" do
|
|
|
531
531
|
end
|
|
532
532
|
end
|
|
533
533
|
|
|
534
|
-
describe "
|
|
534
|
+
describe "resolve_data_source_name" do
|
|
535
535
|
before(:all) do
|
|
536
|
-
|
|
536
|
+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
|
|
537
|
+
@conn = ActiveRecord::Base.connection
|
|
537
538
|
@owner = CONNECTION_PARAMS[:username].upcase
|
|
538
539
|
end
|
|
539
540
|
|
|
540
|
-
|
|
541
|
-
@conn.
|
|
542
|
-
expect(@conn.describe("test_employees")).to eq([@owner, "TEST_EMPLOYEES"])
|
|
543
|
-
@conn.exec "DROP TABLE test_employees" rescue nil
|
|
541
|
+
def resolve(name)
|
|
542
|
+
@conn.send(:resolve_data_source_name, name)
|
|
544
543
|
end
|
|
545
544
|
|
|
546
|
-
it "should
|
|
547
|
-
|
|
545
|
+
it "should resolve existing table" do
|
|
546
|
+
@conn.execute "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
|
|
547
|
+
expect(resolve("test_employees")).to eq([@owner, "TEST_EMPLOYEES"])
|
|
548
|
+
@conn.execute "DROP TABLE test_employees" rescue nil
|
|
548
549
|
end
|
|
549
550
|
|
|
550
|
-
it "should
|
|
551
|
-
expect(
|
|
551
|
+
it "should not resolve non-existing table" do
|
|
552
|
+
expect { resolve("test_xxx") }.to raise_error(ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException)
|
|
552
553
|
end
|
|
553
554
|
|
|
554
|
-
it "should
|
|
555
|
-
expect(
|
|
555
|
+
it "should resolve table in other schema" do
|
|
556
|
+
expect(resolve("sys.dual")).to eq(["SYS", "DUAL"])
|
|
556
557
|
end
|
|
557
558
|
|
|
558
|
-
it "should
|
|
559
|
-
|
|
560
|
-
@conn.exec "CREATE VIEW test_employees_v AS SELECT * FROM test_employees" rescue nil
|
|
561
|
-
expect(@conn.describe("test_employees_v")).to eq([@owner, "TEST_EMPLOYEES_V"])
|
|
562
|
-
@conn.exec "DROP VIEW test_employees_v" rescue nil
|
|
563
|
-
@conn.exec "DROP TABLE test_employees" rescue nil
|
|
559
|
+
it "should resolve table in other schema if the schema and table are in different cases" do
|
|
560
|
+
expect(resolve("SYS.dual")).to eq(["SYS", "DUAL"])
|
|
564
561
|
end
|
|
565
562
|
|
|
566
|
-
it "should
|
|
567
|
-
|
|
563
|
+
it "should resolve existing view" do
|
|
564
|
+
@conn.execute "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
|
|
565
|
+
@conn.execute "CREATE VIEW test_employees_v AS SELECT * FROM test_employees" rescue nil
|
|
566
|
+
expect(resolve("test_employees_v")).to eq([@owner, "TEST_EMPLOYEES_V"])
|
|
567
|
+
@conn.execute "DROP VIEW test_employees_v" rescue nil
|
|
568
|
+
@conn.execute "DROP TABLE test_employees" rescue nil
|
|
568
569
|
end
|
|
569
570
|
|
|
570
|
-
it "should
|
|
571
|
-
|
|
572
|
-
expect(@conn.describe("test_dual")).to eq(["SYS", "DUAL"])
|
|
573
|
-
@conn.exec "DROP SYNONYM test_dual" rescue nil
|
|
571
|
+
it "should resolve view in other schema" do
|
|
572
|
+
expect(resolve("sys.v_$version")).to eq(["SYS", "V_$VERSION"])
|
|
574
573
|
end
|
|
575
574
|
|
|
576
|
-
it "should
|
|
577
|
-
|
|
575
|
+
it "should resolve existing materialized view" do
|
|
576
|
+
@conn.execute "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
|
|
577
|
+
@conn.execute "CREATE MATERIALIZED VIEW test_employees_mv AS SELECT * FROM test_employees" rescue nil
|
|
578
|
+
expect(resolve("test_employees_mv")).to eq([@owner, "TEST_EMPLOYEES_MV"])
|
|
579
|
+
@conn.execute "DROP MATERIALIZED VIEW test_employees_mv" rescue nil
|
|
580
|
+
@conn.execute "DROP TABLE test_employees" rescue nil
|
|
578
581
|
end
|
|
579
582
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
583
|
+
it "should resolve existing private synonym" do
|
|
584
|
+
@conn.execute "CREATE SYNONYM test_dual FOR sys.dual" rescue nil
|
|
585
|
+
expect(resolve("test_dual")).to eq(["SYS", "DUAL"])
|
|
586
|
+
@conn.execute "DROP SYNONYM test_dual" rescue nil
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
it "should resolve existing public synonym" do
|
|
590
|
+
expect(resolve("all_tables")).to eq(["SYS", "ALL_TABLES"])
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Exercises all five catalog paths (table, view, materialized view,
|
|
594
|
+
# private synonym, public synonym) for one underlying table in a single
|
|
595
|
+
# run. The individual cases above use disjoint fixtures; this one proves
|
|
596
|
+
# the DECODE-ordered all_objects lookup + synonym follow-through stays
|
|
597
|
+
# consistent when a private and a public synonym to the same table
|
|
598
|
+
# coexist, and that a materialized view created on the same base table
|
|
599
|
+
# resolves to the MV name (not the base table) as a sibling data source.
|
|
600
|
+
it "resolves table, view, materialized view, private synonym and public synonym for the same underlying table" do
|
|
601
|
+
@conn.execute "CREATE TABLE test_describe_all (id NUMBER)" rescue nil
|
|
602
|
+
@conn.execute "CREATE VIEW test_describe_all_v AS SELECT * FROM test_describe_all" rescue nil
|
|
603
|
+
@conn.execute "CREATE MATERIALIZED VIEW test_describe_all_mv AS SELECT * FROM test_describe_all" rescue nil
|
|
604
|
+
@conn.execute "CREATE SYNONYM test_describe_all_syn FOR test_describe_all" rescue nil
|
|
605
|
+
@conn.execute "CREATE PUBLIC SYNONYM test_describe_all_pub FOR #{@owner}.test_describe_all" rescue nil
|
|
606
|
+
|
|
607
|
+
expect(resolve("test_describe_all")).to eq([@owner, "TEST_DESCRIBE_ALL"])
|
|
608
|
+
expect(resolve("test_describe_all_v")).to eq([@owner, "TEST_DESCRIBE_ALL_V"])
|
|
609
|
+
expect(resolve("test_describe_all_mv")).to eq([@owner, "TEST_DESCRIBE_ALL_MV"])
|
|
610
|
+
expect(resolve("test_describe_all_syn")).to eq([@owner, "TEST_DESCRIBE_ALL"])
|
|
611
|
+
expect(resolve("test_describe_all_pub")).to eq([@owner, "TEST_DESCRIBE_ALL"])
|
|
612
|
+
ensure
|
|
613
|
+
@conn.execute "DROP PUBLIC SYNONYM test_describe_all_pub" rescue nil
|
|
614
|
+
@conn.execute "DROP SYNONYM test_describe_all_syn" rescue nil
|
|
615
|
+
@conn.execute "DROP MATERIALIZED VIEW test_describe_all_mv" rescue nil
|
|
616
|
+
@conn.execute "DROP VIEW test_describe_all_v" rescue nil
|
|
617
|
+
@conn.execute "DROP TABLE test_describe_all" rescue nil
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it "raises when synonym resolution produces a looping chain" do
|
|
621
|
+
@conn.execute "CREATE SYNONYM test_cycle_a FOR test_cycle_b" rescue nil
|
|
622
|
+
@conn.execute "CREATE SYNONYM test_cycle_b FOR test_cycle_a" rescue nil
|
|
623
|
+
expect { resolve("test_cycle_a") }.to raise_error(
|
|
624
|
+
ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException,
|
|
625
|
+
/looping chain of synonyms/
|
|
626
|
+
)
|
|
627
|
+
ensure
|
|
628
|
+
@conn.execute "DROP SYNONYM test_cycle_a" rescue nil
|
|
629
|
+
@conn.execute "DROP SYNONYM test_cycle_b" rescue nil
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
it "raises when a multi-hop synonym chain eventually revisits an earlier link" do
|
|
633
|
+
@conn.execute "CREATE SYNONYM test_cycle_a FOR test_cycle_b" rescue nil
|
|
634
|
+
@conn.execute "CREATE SYNONYM test_cycle_b FOR test_cycle_c" rescue nil
|
|
635
|
+
@conn.execute "CREATE SYNONYM test_cycle_c FOR test_cycle_a" rescue nil
|
|
636
|
+
expect { resolve("test_cycle_a") }.to raise_error(
|
|
637
|
+
ActiveRecord::ConnectionAdapters::OracleEnhanced::ConnectionException,
|
|
638
|
+
/looping chain of synonyms/
|
|
639
|
+
)
|
|
640
|
+
ensure
|
|
641
|
+
@conn.execute "DROP SYNONYM test_cycle_a" rescue nil
|
|
642
|
+
@conn.execute "DROP SYNONYM test_cycle_b" rescue nil
|
|
643
|
+
@conn.execute "DROP SYNONYM test_cycle_c" rescue nil
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
it "raises ArgumentError when the name contains a db link" do
|
|
647
|
+
expect { resolve("test@db_link") }.to raise_error(ArgumentError, /db link is not supported/)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# The previous Connection#describe path bypassed the adapter's query machinery
|
|
651
|
+
# by driving a raw cursor, so its catalog lookup produced no sql.active_record
|
|
652
|
+
# event. Routing through select_one(..., "SCHEMA", ...) makes the lookup
|
|
653
|
+
# participate in logging, instrumentation, and the query cache. Lock that in
|
|
654
|
+
# so a future refactor can't silently regress to the raw-cursor path.
|
|
655
|
+
it "emits a SCHEMA sql.active_record event for the catalog lookup" do
|
|
656
|
+
@conn.execute "CREATE TABLE test_employees (first_name VARCHAR2(20))" rescue nil
|
|
657
|
+
events = []
|
|
658
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
|
|
659
|
+
events << payload
|
|
586
660
|
end
|
|
661
|
+
resolve("test_employees")
|
|
662
|
+
expect(events.map { |p| p[:name] }).to include("SCHEMA")
|
|
663
|
+
ensure
|
|
664
|
+
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
|
|
665
|
+
@conn.execute "DROP TABLE test_employees" rescue nil
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
describe "extract_schema_qualified_name" do
|
|
670
|
+
before(:all) do
|
|
671
|
+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
|
|
672
|
+
@conn = ActiveRecord::Base.connection
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def extract(string)
|
|
676
|
+
@conn.send(:extract_schema_qualified_name, string)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
it "returns [nil, identifier] for an unqualified name and upcases it" do
|
|
680
|
+
expect(extract("table_name")).to eq([nil, "TABLE_NAME"])
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it "leaves an already upcased unqualified name as-is" do
|
|
684
|
+
expect(extract("TABLE_NAME")).to eq([nil, "TABLE_NAME"])
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
it "splits a schema-qualified name and upcases it" do
|
|
688
|
+
expect(extract("hr.dept")).to eq(["HR", "DEPT"])
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
it "upcases a qualified name whose parts are in different cases" do
|
|
692
|
+
expect(extract("SYS.dual")).to eq(["SYS", "DUAL"])
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it "accepts a Symbol and coerces it to a string" do
|
|
696
|
+
expect(extract(:dept)).to eq([nil, "DEPT"])
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
it "raises ArgumentError when the name contains a db link" do
|
|
700
|
+
expect { extract("test@db_link") }.to raise_error(ArgumentError, /db link is not supported/)
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
it "does not upcase a name that is not a valid identifier" do
|
|
704
|
+
expect(extract('"Weird Name"')).to eq([nil, '"Weird Name"'])
|
|
587
705
|
end
|
|
588
706
|
end
|
|
589
707
|
end
|
|
@@ -296,6 +296,7 @@ describe "OracleEnhancedAdapter schema definition" do
|
|
|
296
296
|
end
|
|
297
297
|
|
|
298
298
|
after(:each) do
|
|
299
|
+
long_name = ("a" * (@conn.sequence_name_length - 3)).to_sym
|
|
299
300
|
schema_define do
|
|
300
301
|
drop_table :test_employees_no_primary_key, if_exists: true
|
|
301
302
|
drop_table :test_employees, if_exists: true
|
|
@@ -303,6 +304,7 @@ describe "OracleEnhancedAdapter schema definition" do
|
|
|
303
304
|
drop_table :test_employees_no_pkey, if_exists: true
|
|
304
305
|
drop_table :new_test_employees_no_pkey, if_exists: true
|
|
305
306
|
drop_table :aaaaaaaaaaaaaaaaaaaaaaaaaaa, if_exists: true
|
|
307
|
+
drop_table long_name, if_exists: true
|
|
306
308
|
end
|
|
307
309
|
end
|
|
308
310
|
|
|
@@ -329,6 +331,26 @@ describe "OracleEnhancedAdapter schema definition" do
|
|
|
329
331
|
@conn.rename_table("test_employees_no_pkey", "new_test_employees_no_pkey")
|
|
330
332
|
end.not_to raise_error
|
|
331
333
|
end
|
|
334
|
+
|
|
335
|
+
it "renames the auto-generated sequence when the source table name is long enough to truncate it" do
|
|
336
|
+
long_source = "a" * (@conn.sequence_name_length - 3)
|
|
337
|
+
schema_define do
|
|
338
|
+
create_table long_source.to_sym, force: true do |t|
|
|
339
|
+
t.string :first_name
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
expected_old_seq = @conn.default_sequence_name(long_source).upcase
|
|
344
|
+
expected_new_seq = @conn.default_sequence_name("new_test_employees").upcase
|
|
345
|
+
|
|
346
|
+
@conn.rename_table(long_source, "new_test_employees")
|
|
347
|
+
|
|
348
|
+
sequences = @conn.select_values(
|
|
349
|
+
"SELECT sequence_name FROM user_sequences WHERE sequence_name IN ('#{expected_old_seq}', '#{expected_new_seq}')"
|
|
350
|
+
)
|
|
351
|
+
expect(sequences).to include(expected_new_seq)
|
|
352
|
+
expect(sequences).not_to include(expected_old_seq)
|
|
353
|
+
end
|
|
332
354
|
end
|
|
333
355
|
|
|
334
356
|
describe "add index" do
|
|
@@ -244,7 +244,7 @@ describe "OracleEnhancedAdapter" do
|
|
|
244
244
|
class ::TestPost < ActiveRecord::Base
|
|
245
245
|
has_many :test_comments
|
|
246
246
|
end
|
|
247
|
-
@ids = (1..
|
|
247
|
+
@ids = (1..2010).to_a
|
|
248
248
|
TestPost.transaction do
|
|
249
249
|
@ids.each do |id|
|
|
250
250
|
TestPost.create!(id: id, title: "Title #{id}")
|
|
@@ -272,6 +272,38 @@ describe "OracleEnhancedAdapter" do
|
|
|
272
272
|
posts = TestPost.where(id: [*@ids, nil]).to_a
|
|
273
273
|
expect(posts.size).to eq(@ids.size)
|
|
274
274
|
end
|
|
275
|
+
|
|
276
|
+
# some frameworks like baby_squeel construct Arel objects directly
|
|
277
|
+
it "should allow more than 1000 items using Arel::Nodes::In" do
|
|
278
|
+
table = TestPost.arel_table
|
|
279
|
+
in_node = Arel::Nodes::In.new(table[:id], @ids)
|
|
280
|
+
query = table.where(in_node).project(Arel.star)
|
|
281
|
+
|
|
282
|
+
sql = query.to_sql
|
|
283
|
+
posts = TestPost.connection.select_all(sql).to_a
|
|
284
|
+
expect(posts.size).to eq(@ids.size)
|
|
285
|
+
|
|
286
|
+
# SQL contains multiple IN clauses (split due to 1000 limit)
|
|
287
|
+
expect(sql.scan(/IN \(/).size).to be > 1
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "should allow more than 1000 items using Arel::Nodes::NotIn" do
|
|
291
|
+
ids = @ids.dup
|
|
292
|
+
non_not_in = ids.pop
|
|
293
|
+
|
|
294
|
+
table = TestPost.arel_table
|
|
295
|
+
not_in_node = Arel::Nodes::NotIn.new(table[:id], ids)
|
|
296
|
+
query = table.where(not_in_node).project(Arel.star)
|
|
297
|
+
|
|
298
|
+
sql = query.to_sql
|
|
299
|
+
posts = TestPost.connection.select_all(sql).to_a
|
|
300
|
+
|
|
301
|
+
expect(posts.size).to eq(1)
|
|
302
|
+
expect(posts.first["id"]).to eq(non_not_in)
|
|
303
|
+
|
|
304
|
+
# SQL contains multiple NOT IN clauses (split due to 1000 limit)
|
|
305
|
+
expect(sql.scan(/NOT IN \(/).size).to be > 1
|
|
306
|
+
end
|
|
275
307
|
end
|
|
276
308
|
|
|
277
309
|
describe "with statement pool" do
|
|
@@ -806,10 +838,54 @@ describe "OracleEnhancedAdapter" do
|
|
|
806
838
|
ActiveRecord::Base.clear_cache!
|
|
807
839
|
end
|
|
808
840
|
|
|
841
|
+
before(:each) do
|
|
842
|
+
TestPost.delete_all
|
|
843
|
+
TestComment.delete_all
|
|
844
|
+
end
|
|
845
|
+
|
|
809
846
|
it "should not raise undefined method length" do
|
|
810
847
|
post = TestPost.create!
|
|
811
848
|
post.test_comments << TestComment.create!
|
|
812
849
|
expect(TestComment.where(test_post_id: TestPost.select(:id)).size).to eq(1)
|
|
813
850
|
end
|
|
851
|
+
|
|
852
|
+
it "should handle IN with subquery using Arel::Nodes::In" do
|
|
853
|
+
post = TestPost.create!
|
|
854
|
+
post.test_comments << TestComment.create!
|
|
855
|
+
|
|
856
|
+
table = TestComment.arel_table
|
|
857
|
+
subquery = TestPost.select(:id).arel
|
|
858
|
+
in_node = Arel::Nodes::In.new(table[:test_post_id], subquery)
|
|
859
|
+
query = table.where(in_node).project(Arel.star)
|
|
860
|
+
|
|
861
|
+
sql = query.to_sql
|
|
862
|
+
comments = TestComment.connection.select_all(sql).to_a
|
|
863
|
+
expect(comments.size).to eq(1)
|
|
864
|
+
|
|
865
|
+
# SQL should contain IN with subquery, not split into multiple IN clauses
|
|
866
|
+
expect(sql).to match(/IN \(+SELECT/)
|
|
867
|
+
expect(sql.scan(/IN \(/).size).to eq(1)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
it "should handle NOT IN with subquery using Arel::Nodes::NotIn" do
|
|
871
|
+
post = TestPost.create!
|
|
872
|
+
TestComment.create!(test_post_id: post.id)
|
|
873
|
+
orphan_comment = TestComment.create!(test_post_id: post.id + 1)
|
|
874
|
+
|
|
875
|
+
table = TestComment.arel_table
|
|
876
|
+
subquery = TestPost.select(:id).arel
|
|
877
|
+
not_in_node = Arel::Nodes::NotIn.new(table[:test_post_id], subquery)
|
|
878
|
+
query = table.where(not_in_node).project(Arel.star)
|
|
879
|
+
|
|
880
|
+
sql = query.to_sql
|
|
881
|
+
comments = TestComment.connection.select_all(sql).to_a
|
|
882
|
+
|
|
883
|
+
expect(comments.size).to eq(1)
|
|
884
|
+
expect(comments.first["id"]).to eq(orphan_comment.id)
|
|
885
|
+
|
|
886
|
+
# SQL should contain NOT IN with subquery, not split into multiple NOT IN clauses
|
|
887
|
+
expect(sql).to match(/NOT IN \(+SELECT/)
|
|
888
|
+
expect(sql.scan(/NOT IN \(/).size).to eq(1)
|
|
889
|
+
end
|
|
814
890
|
end
|
|
815
891
|
end
|
|
@@ -4,28 +4,12 @@ CREATE USER oracle_enhanced IDENTIFIED BY oracle_enhanced;
|
|
|
4
4
|
|
|
5
5
|
GRANT unlimited tablespace, create session, create table, create sequence,
|
|
6
6
|
create procedure, create trigger, create view, create materialized view,
|
|
7
|
-
create database link, create synonym, create type, ctxapp
|
|
7
|
+
create database link, create synonym, create type, ctxapp,
|
|
8
|
+
create public synonym, drop public synonym TO oracle_enhanced;
|
|
8
9
|
|
|
9
10
|
CREATE USER oracle_enhanced_schema IDENTIFIED BY oracle_enhanced_schema;
|
|
10
11
|
|
|
11
12
|
GRANT unlimited tablespace, create session, create table, create sequence,
|
|
12
13
|
create procedure, create trigger, create view, create materialized view,
|
|
13
|
-
create database link, create synonym, create type, ctxapp
|
|
14
|
-
|
|
15
|
-
CREATE USER arunit IDENTIFIED BY arunit;
|
|
16
|
-
|
|
17
|
-
GRANT unlimited tablespace, create session, create table, create sequence,
|
|
18
|
-
create procedure, create trigger, create view, create materialized view,
|
|
19
|
-
create database link, create synonym, create type, ctxapp TO arunit;
|
|
20
|
-
|
|
21
|
-
CREATE USER arunit2 IDENTIFIED BY arunit2;
|
|
22
|
-
|
|
23
|
-
GRANT unlimited tablespace, create session, create table, create sequence,
|
|
24
|
-
create procedure, create trigger, create view, create materialized view,
|
|
25
|
-
create database link, create synonym, create type, ctxapp TO arunit2;
|
|
26
|
-
|
|
27
|
-
CREATE USER ruby IDENTIFIED BY oci8;
|
|
28
|
-
GRANT connect, resource, create view,create synonym TO ruby;
|
|
29
|
-
GRANT EXECUTE ON dbms_lock TO ruby;
|
|
30
|
-
GRANT CREATE VIEW TO ruby;
|
|
31
|
-
GRANT unlimited tablespace to ruby;
|
|
14
|
+
create database link, create synonym, create type, ctxapp,
|
|
15
|
+
create public synonym, drop public synonym TO oracle_enhanced_schema;
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-oracle_enhanced-adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.1.
|
|
4
|
+
version: 8.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raimonds Simanovskis
|
|
@@ -103,6 +103,7 @@ files:
|
|
|
103
103
|
- lib/arel/visitors/oracle12.rb
|
|
104
104
|
- lib/arel/visitors/oracle_common.rb
|
|
105
105
|
- spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb
|
|
106
|
+
- spec/active_record/connection_adapters/oracle_enhanced/columns_for_distinct_spec.rb
|
|
106
107
|
- spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb
|
|
107
108
|
- spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb
|
|
108
109
|
- spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb
|
|
@@ -155,11 +156,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
155
156
|
- !ruby/object:Gem::Version
|
|
156
157
|
version: 1.8.11
|
|
157
158
|
requirements: []
|
|
158
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.11
|
|
159
160
|
specification_version: 4
|
|
160
161
|
summary: Oracle enhanced adapter for ActiveRecord
|
|
161
162
|
test_files:
|
|
162
163
|
- spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb
|
|
164
|
+
- spec/active_record/connection_adapters/oracle_enhanced/columns_for_distinct_spec.rb
|
|
163
165
|
- spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb
|
|
164
166
|
- spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb
|
|
165
167
|
- spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb
|