activerecord-oracle_enhanced-adapter 8.1.0-java

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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1971 -0
  3. data/License.txt +20 -0
  4. data/README.md +947 -0
  5. data/VERSION +1 -0
  6. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +7 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +24 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +137 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +359 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced/database_limits.rb +47 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +325 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb +63 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +71 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +629 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb +38 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +57 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +465 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb +44 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb +195 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +186 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +95 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +99 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb +197 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +739 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +394 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced/type_metadata.rb +34 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +3 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +886 -0
  29. data/lib/active_record/type/oracle_enhanced/boolean.rb +19 -0
  30. data/lib/active_record/type/oracle_enhanced/character_string.rb +36 -0
  31. data/lib/active_record/type/oracle_enhanced/integer.rb +14 -0
  32. data/lib/active_record/type/oracle_enhanced/json.rb +10 -0
  33. data/lib/active_record/type/oracle_enhanced/national_character_string.rb +26 -0
  34. data/lib/active_record/type/oracle_enhanced/national_character_text.rb +36 -0
  35. data/lib/active_record/type/oracle_enhanced/raw.rb +25 -0
  36. data/lib/active_record/type/oracle_enhanced/string.rb +29 -0
  37. data/lib/active_record/type/oracle_enhanced/text.rb +32 -0
  38. data/lib/active_record/type/oracle_enhanced/timestampltz.rb +25 -0
  39. data/lib/active_record/type/oracle_enhanced/timestamptz.rb +25 -0
  40. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  41. data/lib/arel/visitors/oracle.rb +216 -0
  42. data/lib/arel/visitors/oracle12.rb +121 -0
  43. data/lib/arel/visitors/oracle_common.rb +51 -0
  44. data/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb +24 -0
  45. data/spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb +40 -0
  46. data/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb +84 -0
  47. data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +589 -0
  48. data/spec/active_record/connection_adapters/oracle_enhanced/context_index_spec.rb +431 -0
  49. data/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb +122 -0
  50. data/spec/active_record/connection_adapters/oracle_enhanced/dbconsole_spec.rb +63 -0
  51. data/spec/active_record/connection_adapters/oracle_enhanced/dbms_output_spec.rb +69 -0
  52. data/spec/active_record/connection_adapters/oracle_enhanced/procedures_spec.rb +362 -0
  53. data/spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb +181 -0
  54. data/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb +492 -0
  55. data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +1318 -0
  56. data/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +485 -0
  57. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +815 -0
  58. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +230 -0
  59. data/spec/active_record/oracle_enhanced/type/binary_spec.rb +119 -0
  60. data/spec/active_record/oracle_enhanced/type/boolean_spec.rb +206 -0
  61. data/spec/active_record/oracle_enhanced/type/character_string_spec.rb +67 -0
  62. data/spec/active_record/oracle_enhanced/type/custom_spec.rb +90 -0
  63. data/spec/active_record/oracle_enhanced/type/decimal_spec.rb +56 -0
  64. data/spec/active_record/oracle_enhanced/type/dirty_spec.rb +141 -0
  65. data/spec/active_record/oracle_enhanced/type/float_spec.rb +48 -0
  66. data/spec/active_record/oracle_enhanced/type/integer_spec.rb +101 -0
  67. data/spec/active_record/oracle_enhanced/type/json_spec.rb +56 -0
  68. data/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +55 -0
  69. data/spec/active_record/oracle_enhanced/type/national_character_text_spec.rb +230 -0
  70. data/spec/active_record/oracle_enhanced/type/raw_spec.rb +137 -0
  71. data/spec/active_record/oracle_enhanced/type/text_spec.rb +295 -0
  72. data/spec/active_record/oracle_enhanced/type/timestamp_spec.rb +107 -0
  73. data/spec/spec_config.yaml.template +11 -0
  74. data/spec/spec_helper.rb +225 -0
  75. data/spec/support/alter_system_set_open_cursors.sql +1 -0
  76. data/spec/support/alter_system_user_password.sql +2 -0
  77. data/spec/support/create_oracle_enhanced_users.sql +31 -0
  78. metadata +181 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 8.1.0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecord::ConnectionAdapters::OracleAdapter < ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter # :nodoc:
4
+ def adapter_name
5
+ "Oracle"
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters # :nodoc:
5
+ module OracleEnhanced
6
+ class Column < ActiveRecord::ConnectionAdapters::Column
7
+ delegate :virtual, to: :sql_type_metadata, allow_nil: true
8
+
9
+ def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, comment: nil) # :nodoc:
10
+ super(name, cast_type, default, sql_type_metadata, null, comment: comment)
11
+ end
12
+
13
+ def virtual?
14
+ virtual
15
+ end
16
+
17
+ def auto_incremented_by_db?
18
+ # TODO: Identify if a column is the primary key and is auto-incremented (e.g. by a sequence)
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # interface independent methods
6
+ module OracleEnhanced
7
+ class Connection # :nodoc:
8
+ def self.create(config)
9
+ case ORACLE_ENHANCED_CONNECTION
10
+ when :oci
11
+ OracleEnhanced::OCIConnection.new(config)
12
+ when :jdbc
13
+ OracleEnhanced::JDBCConnection.new(config)
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ attr_reader :raw_connection
20
+
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
+ # Oracle column names by default are case-insensitive, but treated as upcase;
70
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
71
+ # their column names when creating Oracle tables, which makes then case-sensitive.
72
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
73
+ # camelCase column name. I imagine other dbs handle this different, since there's a
74
+ # 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
+ def _oracle_downcase(column_name)
83
+ return nil if column_name.nil?
84
+ /[a-z]/.match?(column_name) ? column_name : column_name.downcase
85
+ 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
+ end
116
+
117
+ # Returns array with major and minor version of database (e.g. [12, 1])
118
+ def database_version
119
+ raise NoMethodError, "Not implemented for this raw driver"
120
+ end
121
+ class ConnectionException < StandardError # :nodoc:
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ # if MRI or YARV or TruffleRuby
128
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "truffleruby"
129
+ ORACLE_ENHANCED_CONNECTION = :oci
130
+ require "active_record/connection_adapters/oracle_enhanced/oci_connection"
131
+ # if JRuby
132
+ elsif RUBY_ENGINE == "jruby"
133
+ ORACLE_ENHANCED_CONNECTION = :jdbc
134
+ require "active_record/connection_adapters/oracle_enhanced/jdbc_connection"
135
+ else
136
+ raise "Unsupported Ruby engine #{RUBY_ENGINE}"
137
+ end
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module OracleEnhanced
6
+ module ContextIndex
7
+ # Define full text index with Oracle specific CONTEXT index type
8
+ #
9
+ # Oracle CONTEXT index by default supports full text indexing of one column.
10
+ # This method allows full text index creation also on several columns
11
+ # as well as indexing related table columns by generating stored procedure
12
+ # that concatenates all columns for indexing as well as generating trigger
13
+ # that will update main index column to trigger reindexing of record.
14
+ #
15
+ # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition
16
+ # and order by score of matched results.
17
+ #
18
+ # Options:
19
+ #
20
+ # * <tt>:name</tt>
21
+ # * <tt>:index_column</tt>
22
+ # * <tt>:index_column_trigger_on</tt>
23
+ # * <tt>:tablespace</tt>
24
+ # * <tt>:sync</tt> - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL').
25
+ # * <tt>:lexer</tt> - Lexer options (e.g. <tt>:type => 'BASIC_LEXER', :base_letter => true</tt>).
26
+ # * <tt>:wordlist</tt> - Wordlist options (e.g. <tt>:type => 'BASIC_WORDLIST', :prefix_index => true</tt>).
27
+ # * <tt>:transactional</tt> - When +true+, the CONTAINS operator will process inserted and updated rows.
28
+ #
29
+ # ===== Examples
30
+ #
31
+ # ====== Creating single column index
32
+ # add_context_index :posts, :title
33
+ # search with
34
+ # Post.contains(:title, 'word')
35
+ #
36
+ # ====== Creating index on several columns
37
+ # add_context_index :posts, [:title, :body]
38
+ # search with (use first column as argument for contains method but it will search in all index columns)
39
+ # Post.contains(:title, 'word')
40
+ #
41
+ # ====== Creating index on several columns with dummy index column and commit option
42
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
43
+ # search with
44
+ # Post.contains(:all_text, 'word')
45
+ #
46
+ # ====== Creating index with trigger option (will reindex when specified columns are updated)
47
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
48
+ # :index_column_trigger_on => [:created_at, :updated_at]
49
+ # search with
50
+ # Post.contains(:all_text, 'word')
51
+ #
52
+ # ====== Creating index on multiple tables
53
+ # add_context_index :posts,
54
+ # [:title, :body,
55
+ # # specify aliases always with AS keyword
56
+ # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
57
+ # ],
58
+ # :name => 'post_and_comments_index',
59
+ # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
60
+ # :sync => 'ON COMMIT'
61
+ # search in any table columns
62
+ # Post.contains(:all_text, 'word')
63
+ # search in specified column
64
+ # Post.contains(:all_text, "aaa within title")
65
+ # Post.contains(:all_text, "bbb within comment_author")
66
+ #
67
+ # ====== Creating index using lexer
68
+ # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... }
69
+ #
70
+ # ====== Creating index using wordlist
71
+ # add_context_index :posts, :title, :wordlist => { :type => 'BASIC_WORDLIST', :prefix_index => true, ... }
72
+ #
73
+ # ====== Creating transactional index (will reindex changed rows when querying)
74
+ # add_context_index :posts, :title, :transactional => true
75
+ #
76
+ def add_context_index(table_name, column_name, options = {})
77
+ column_names = Array(column_name)
78
+ index_name = options[:name] || index_name(table_name, column: options[:index_column] || column_names,
79
+ # CONEXT index name max length is 25
80
+ identifier_max_length: 25)
81
+
82
+ quoted_column_name = quote_column_name(options[:index_column] || column_names.first)
83
+ if options[:index_column_trigger_on]
84
+ raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \
85
+ unless options[:index_column]
86
+ create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on])
87
+ end
88
+
89
+ sql = +"CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
90
+ sql << " (#{quoted_column_name})"
91
+ sql << " INDEXTYPE IS CTXSYS.CONTEXT"
92
+ parameters = []
93
+ if column_names.size > 1
94
+ procedure_name = default_datastore_procedure(index_name)
95
+ datastore_name = default_datastore_name(index_name)
96
+ create_datastore_procedure(table_name, procedure_name, column_names, options)
97
+ create_datastore_preference(datastore_name, procedure_name)
98
+ parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP"
99
+ end
100
+ if options[:tablespace]
101
+ storage_name = default_storage_name(index_name)
102
+ create_storage_preference(storage_name, options[:tablespace])
103
+ parameters << "STORAGE #{storage_name}"
104
+ end
105
+ if options[:sync]
106
+ parameters << "SYNC(#{options[:sync]})"
107
+ end
108
+ if options[:lexer] && (lexer_type = options[:lexer][:type])
109
+ lexer_name = default_lexer_name(index_name)
110
+ (lexer_options = options[:lexer].dup).delete(:type)
111
+ create_lexer_preference(lexer_name, lexer_type, lexer_options)
112
+ parameters << "LEXER #{lexer_name}"
113
+ end
114
+ if options[:wordlist] && (wordlist_type = options[:wordlist][:type])
115
+ wordlist_name = default_wordlist_name(index_name)
116
+ (wordlist_options = options[:wordlist].dup).delete(:type)
117
+ create_wordlist_preference(wordlist_name, wordlist_type, wordlist_options)
118
+ parameters << "WORDLIST #{wordlist_name}"
119
+ end
120
+ if options[:transactional]
121
+ parameters << "TRANSACTIONAL"
122
+ end
123
+ unless parameters.empty?
124
+ sql << " PARAMETERS ('#{parameters.join(' ')}')"
125
+ end
126
+ execute sql
127
+ end
128
+
129
+ # Drop full text index with Oracle specific CONTEXT index type
130
+ def remove_context_index(table_name, options = {})
131
+ unless Hash === options # if column names passed as argument
132
+ options = { column: Array(options) }
133
+ end
134
+ index_name = options[:name] || index_name(table_name,
135
+ column: options[:index_column] || options[:column], identifier_max_length: 25)
136
+ execute "DROP INDEX #{index_name}"
137
+ drop_ctx_preference(default_datastore_name(index_name))
138
+ drop_ctx_preference(default_storage_name(index_name))
139
+ procedure_name = default_datastore_procedure(index_name)
140
+ execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
141
+ drop_index_column_trigger(index_name)
142
+ end
143
+
144
+ private
145
+ def create_datastore_procedure(table_name, procedure_name, column_names, options)
146
+ quoted_table_name = quote_table_name(table_name)
147
+ select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i }
148
+ select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, " ") }
149
+ keys, selected_columns = parse_select_queries(select_queries)
150
+ quoted_column_names = (column_names + keys).map { |col| quote_column_name(col) }
151
+ execute <<~SQL
152
+ CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
153
+ (p_rowid IN ROWID,
154
+ p_clob IN OUT NOCOPY CLOB) IS
155
+ -- add_context_index_parameters #{(column_names + select_queries).inspect}#{!options.empty? ? +', ' << options.inspect[1..-2] : ''}
156
+ #{
157
+ selected_columns.map do |cols|
158
+ cols.map do |col|
159
+ raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
160
+ "l_#{col} VARCHAR2(32767);\n"
161
+ end.join
162
+ end.join
163
+ } BEGIN
164
+ FOR r1 IN (
165
+ SELECT #{quoted_column_names.join(', ')}
166
+ FROM #{quoted_table_name}
167
+ WHERE #{quoted_table_name}.ROWID = p_rowid
168
+ ) LOOP
169
+ #{
170
+ (column_names.map do |col|
171
+ col = col.to_s
172
+ +"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 2}, '<#{col}>');\n" <<
173
+ "IF LENGTH(r1.#{col}) > 0 THEN\n" <<
174
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
175
+ "END IF;\n" <<
176
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 3}, '</#{col}>');\n"
177
+ end.join) <<
178
+ (selected_columns.zip(select_queries).map do |cols, query|
179
+ (cols.map do |col|
180
+ "l_#{col} := '';\n"
181
+ end.join) <<
182
+ "FOR r2 IN (\n" <<
183
+ query.gsub(/:(\w+)/, "r1.\\1") << "\n) LOOP\n" <<
184
+ (cols.map do |col|
185
+ "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
186
+ end.join) <<
187
+ "END LOOP;\n" <<
188
+ (cols.map do |col|
189
+ col = col.to_s
190
+ +"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 2}, '<#{col}>');\n" <<
191
+ "IF LENGTH(l_#{col}) > 0 THEN\n" <<
192
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
193
+ "END IF;\n" <<
194
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 3}, '</#{col}>');\n"
195
+ end.join)
196
+ end.join)
197
+ }
198
+ END LOOP;
199
+ END;
200
+ SQL
201
+ end
202
+
203
+ def parse_select_queries(select_queries)
204
+ keys = []
205
+ selected_columns = []
206
+ select_queries.each do |query|
207
+ # get primary or foreign keys like :id or :something_id
208
+ keys << (query.scan(/:\w+/).map { |k| k[1..-1].downcase.to_sym })
209
+ select_part = query.scan(/^select\s.*\sfrom/i).first
210
+ selected_columns << select_part.scan(/\sas\s+(\w+)/i).map { |c| c.first }
211
+ end
212
+ [keys.flatten.uniq, selected_columns]
213
+ end
214
+
215
+ def create_datastore_preference(datastore_name, procedure_name)
216
+ drop_ctx_preference(datastore_name)
217
+ execute <<~SQL
218
+ BEGIN
219
+ CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
220
+ CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
221
+ END;
222
+ SQL
223
+ end
224
+
225
+ def create_storage_preference(storage_name, tablespace)
226
+ drop_ctx_preference(storage_name)
227
+ sql = +"BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
228
+ ["I_TABLE_CLAUSE", "K_TABLE_CLAUSE", "R_TABLE_CLAUSE",
229
+ "N_TABLE_CLAUSE", "I_INDEX_CLAUSE", "P_TABLE_CLAUSE"].each do |clause|
230
+ default_clause = case clause
231
+ when "R_TABLE_CLAUSE"; "LOB(DATA) STORE AS (CACHE) "
232
+ when "I_INDEX_CLAUSE"; "COMPRESS 2 "
233
+ else ""
234
+ end
235
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
236
+ end
237
+ sql << "END;\n"
238
+ execute sql
239
+ end
240
+
241
+ def create_lexer_preference(lexer_name, lexer_type, options)
242
+ drop_ctx_preference(lexer_name)
243
+ sql = +"BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n"
244
+ options.each do |key, value|
245
+ plsql_value = case value
246
+ when String; "'#{value}'"
247
+ when true; "'YES'"
248
+ when false; "'NO'"
249
+ when nil; "NULL"
250
+ else value
251
+ end
252
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
253
+ end
254
+ sql << "END;\n"
255
+ execute sql
256
+ end
257
+
258
+ def create_wordlist_preference(wordlist_name, wordlist_type, options)
259
+ drop_ctx_preference(wordlist_name)
260
+ sql = +"BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n"
261
+ options.each do |key, value|
262
+ plsql_value = case value
263
+ when String; "'#{value}'"
264
+ when true; "'YES'"
265
+ when false; "'NO'"
266
+ when nil; "NULL"
267
+ else value
268
+ end
269
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n"
270
+ end
271
+ sql << "END;\n"
272
+ execute sql
273
+ end
274
+
275
+ def drop_ctx_preference(preference_name)
276
+ execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
277
+ end
278
+
279
+ def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
280
+ trigger_name = default_index_column_trigger_name(index_name)
281
+ columns = Array(index_column_source)
282
+ quoted_column_names = columns.map { |col| quote_column_name(col) }.join(", ")
283
+ execute <<~SQL
284
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
285
+ BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
286
+ BEGIN
287
+ :new.#{quote_column_name(index_column)} := '1';
288
+ END;
289
+ SQL
290
+ end
291
+
292
+ def drop_index_column_trigger(index_name)
293
+ trigger_name = default_index_column_trigger_name(index_name)
294
+ execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
295
+ end
296
+
297
+ def default_datastore_procedure(index_name)
298
+ "#{index_name}_prc"
299
+ end
300
+
301
+ def default_datastore_name(index_name)
302
+ "#{index_name}_dst"
303
+ end
304
+
305
+ def default_storage_name(index_name)
306
+ "#{index_name}_sto"
307
+ end
308
+
309
+ def default_index_column_trigger_name(index_name)
310
+ "#{index_name}_trg"
311
+ end
312
+
313
+ def default_lexer_name(index_name)
314
+ "#{index_name}_lex"
315
+ end
316
+
317
+ def default_wordlist_name(index_name)
318
+ "#{index_name}_wl"
319
+ end
320
+
321
+ module BaseClassMethods
322
+ # Declare that model table has context index defined.
323
+ # As a result <tt>contains</tt> class scope method is defined.
324
+ def has_context_index
325
+ extend ContextIndexClassMethods
326
+ end
327
+ end
328
+
329
+ module ContextIndexClassMethods
330
+ # Add context index condition.
331
+ def contains(column, query, options = {})
332
+ score_label = options[:label].to_i || 1
333
+ quoted_column = connection.quote_table_name(column)
334
+
335
+ # Create an Arel node for the CONTAINS function
336
+ contains_node = Arel::Nodes::NamedFunction.new(
337
+ "CONTAINS",
338
+ [
339
+ Arel::Nodes::SqlLiteral.new(quoted_column),
340
+ Arel::Nodes::BindParam.new(query),
341
+ Arel::Nodes::SqlLiteral.new(score_label.to_s)
342
+ ]
343
+ )
344
+
345
+ # Create comparison node: CONTAINS(...) > 0
346
+ condition = Arel::Nodes::GreaterThan.new(contains_node, Arel::Nodes::SqlLiteral.new("0"))
347
+
348
+ # Create the where clause and order by score
349
+ where(condition).order(Arel.sql("SCORE(#{score_label}) DESC"))
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ ActiveRecord::Base.class_eval do
358
+ extend ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex::BaseClassMethods
359
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/deprecation"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module OracleEnhanced
8
+ module DatabaseLimits
9
+ # maximum length of Oracle identifiers
10
+ IDENTIFIER_MAX_LENGTH = 30
11
+
12
+ def table_alias_length # :nodoc:
13
+ IDENTIFIER_MAX_LENGTH
14
+ end
15
+
16
+ # the maximum length of a table name
17
+ def table_name_length
18
+ IDENTIFIER_MAX_LENGTH
19
+ end
20
+ deprecate :table_name_length, deprecator: ActiveSupport::Deprecation.new
21
+
22
+ # the maximum length of a column name
23
+ def column_name_length
24
+ IDENTIFIER_MAX_LENGTH
25
+ end
26
+ deprecate :column_name_length, deprecator: ActiveSupport::Deprecation.new
27
+
28
+ # the maximum length of an index name
29
+ # supported by this database
30
+ def index_name_length
31
+ IDENTIFIER_MAX_LENGTH
32
+ end
33
+
34
+ # the maximum length of a sequence name
35
+ def sequence_name_length
36
+ IDENTIFIER_MAX_LENGTH
37
+ end
38
+
39
+ # To avoid ORA-01795: maximum number of expressions in a list is 1000
40
+ # tell ActiveRecord to limit us to 1000 ids at a time
41
+ def in_clause_length
42
+ 1000
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end