activerecord-oracle_enhanced-adapter 1.4.3 → 5.2.0

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 (100) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +1162 -2
  3. data/README.md +567 -155
  4. data/VERSION +1 -1
  5. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +3 -1
  6. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +19 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +132 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +345 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced/database_limits.rb +52 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +280 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb +64 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +59 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +538 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb +38 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +46 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +435 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb +44 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb +196 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +164 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +95 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +79 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb +194 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +709 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +28 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +353 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced/type_metadata.rb +33 -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 +385 -1083
  29. data/lib/active_record/type/oracle_enhanced/boolean.rb +20 -0
  30. data/lib/active_record/type/oracle_enhanced/integer.rb +15 -0
  31. data/lib/active_record/type/oracle_enhanced/json.rb +10 -0
  32. data/lib/active_record/type/oracle_enhanced/national_character_string.rb +26 -0
  33. data/lib/active_record/type/oracle_enhanced/national_character_text.rb +36 -0
  34. data/lib/active_record/type/oracle_enhanced/raw.rb +25 -0
  35. data/lib/active_record/type/oracle_enhanced/string.rb +29 -0
  36. data/lib/active_record/type/oracle_enhanced/text.rb +32 -0
  37. data/lib/active_record/type/oracle_enhanced/timestampltz.rb +25 -0
  38. data/lib/active_record/type/oracle_enhanced/timestamptz.rb +25 -0
  39. data/lib/activerecord-oracle_enhanced-adapter.rb +5 -13
  40. data/spec/active_record/connection_adapters/{oracle_enhanced_emulate_oracle_adapter_spec.rb → emulation/oracle_adapter_spec.rb} +5 -4
  41. data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +469 -0
  42. data/spec/active_record/connection_adapters/{oracle_enhanced_context_index_spec.rb → oracle_enhanced/context_index_spec.rb} +140 -128
  43. data/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb +112 -0
  44. data/spec/active_record/connection_adapters/{oracle_enhanced_dbms_output_spec.rb → oracle_enhanced/dbms_output_spec.rb} +13 -13
  45. data/spec/active_record/connection_adapters/oracle_enhanced/procedures_spec.rb +365 -0
  46. data/spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb +196 -0
  47. data/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb +492 -0
  48. data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +1433 -0
  49. data/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +478 -0
  50. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +385 -550
  51. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +92 -1249
  52. data/spec/active_record/oracle_enhanced/type/binary_spec.rb +119 -0
  53. data/spec/active_record/oracle_enhanced/type/boolean_spec.rb +208 -0
  54. data/spec/active_record/oracle_enhanced/type/dirty_spec.rb +139 -0
  55. data/spec/active_record/oracle_enhanced/type/float_spec.rb +48 -0
  56. data/spec/active_record/oracle_enhanced/type/integer_spec.rb +91 -0
  57. data/spec/active_record/oracle_enhanced/type/json_spec.rb +57 -0
  58. data/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +55 -0
  59. data/spec/active_record/oracle_enhanced/type/national_character_text_spec.rb +230 -0
  60. data/spec/active_record/oracle_enhanced/type/raw_spec.rb +122 -0
  61. data/spec/active_record/oracle_enhanced/type/text_spec.rb +229 -0
  62. data/spec/active_record/oracle_enhanced/type/timestamp_spec.rb +75 -0
  63. data/spec/spec_config.yaml.template +11 -0
  64. data/spec/spec_helper.rb +100 -93
  65. data/spec/support/alter_system_set_open_cursors.sql +1 -0
  66. data/spec/support/alter_system_user_password.sql +2 -0
  67. data/spec/support/create_oracle_enhanced_users.sql +31 -0
  68. metadata +105 -152
  69. data/.rspec +0 -2
  70. data/Gemfile +0 -52
  71. data/RUNNING_TESTS.md +0 -45
  72. data/Rakefile +0 -59
  73. data/activerecord-oracle_enhanced-adapter.gemspec +0 -130
  74. data/lib/active_record/connection_adapters/oracle_enhanced.rake +0 -105
  75. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +0 -41
  76. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +0 -121
  77. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +0 -151
  78. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +0 -119
  79. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +0 -359
  80. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +0 -25
  81. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +0 -21
  82. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +0 -46
  83. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +0 -572
  84. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +0 -497
  85. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +0 -260
  86. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +0 -227
  87. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +0 -260
  88. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +0 -428
  89. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +0 -258
  90. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +0 -294
  91. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +0 -17
  92. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +0 -1
  93. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +0 -334
  94. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +0 -19
  95. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +0 -113
  96. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +0 -141
  97. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +0 -378
  98. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +0 -440
  99. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +0 -1400
  100. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +0 -339
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.3
1
+ 5.2.0
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ActiveRecord::ConnectionAdapters::OracleAdapter < ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter #:nodoc:
2
4
  def adapter_name
3
- 'Oracle'
5
+ "Oracle"
4
6
  end
5
7
  end
@@ -0,0 +1,19 @@
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, default, sql_type_metadata = nil, null = true, table_name = nil, comment = nil) #:nodoc:
10
+ super(name, default, sql_type_metadata, null, table_name, comment: comment)
11
+ end
12
+
13
+ def virtual?
14
+ virtual
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,132 @@
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
+
23
+ # Used always by JDBC connection as well by OCI connection when describing tables over database link
24
+ def describe(name)
25
+ name = name.to_s
26
+ if name.include?("@")
27
+ name, db_link = name.split("@")
28
+ default_owner = _select_value("SELECT username FROM all_db_links WHERE db_link = '#{db_link.upcase}'")
29
+ db_link = "@#{db_link}"
30
+ else
31
+ db_link = nil
32
+ default_owner = @owner
33
+ end
34
+ real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
35
+ if real_name.include?(".")
36
+ table_owner, table_name = real_name.split(".")
37
+ else
38
+ table_owner, table_name = default_owner, real_name
39
+ end
40
+ sql = <<-SQL.strip.gsub(/\s+/, " ")
41
+ SELECT owner, table_name, 'TABLE' name_type
42
+ FROM all_tables#{db_link}
43
+ WHERE owner = '#{table_owner}'
44
+ AND table_name = '#{table_name}'
45
+ UNION ALL
46
+ SELECT owner, view_name table_name, 'VIEW' name_type
47
+ FROM all_views#{db_link}
48
+ WHERE owner = '#{table_owner}'
49
+ AND view_name = '#{table_name}'
50
+ UNION ALL
51
+ SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type
52
+ FROM all_synonyms#{db_link}
53
+ WHERE owner = '#{table_owner}'
54
+ AND synonym_name = '#{table_name}'
55
+ UNION ALL
56
+ SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type
57
+ FROM all_synonyms#{db_link}
58
+ WHERE owner = 'PUBLIC'
59
+ AND synonym_name = '#{real_name}'
60
+ SQL
61
+ if result = _select_one(sql)
62
+ case result["name_type"]
63
+ when "SYNONYM"
64
+ describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}#{db_link}")
65
+ else
66
+ db_link ? [result["owner"], result["table_name"], db_link] : [result["owner"], result["table_name"]]
67
+ end
68
+ else
69
+ raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
70
+ end
71
+ end
72
+
73
+ # Oracle column names by default are case-insensitive, but treated as upcase;
74
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
75
+ # their column names when creating Oracle tables, which makes then case-sensitive.
76
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
77
+ # camelCase column name. I imagine other dbs handle this different, since there's a
78
+ # unit test that's currently failing test_oci.
79
+ #
80
+ # `_oracle_downcase` is expected to be called only from
81
+ # `ActiveRecord::ConnectionAdapters::OracleEnhanced::OCIConnection`
82
+ # or `ActiveRecord::ConnectionAdapters::OracleEnhanced::JDBCConnection`.
83
+ # Other method should call `ActiveRecord:: ConnectionAdapters::OracleEnhanced::Quoting#oracle_downcase`
84
+ # since this is kind of quoting, not connection.
85
+ # To avoid it is called from anywhere else, added _ at the beginning of the method name.
86
+ def _oracle_downcase(column_name)
87
+ return nil if column_name.nil?
88
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
89
+ end
90
+
91
+ # _select_one and _select_value methods are expected to be called
92
+ # only from `ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection#describe`
93
+ # Other methods should call `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_one`
94
+ # and `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_value`
95
+ # To avoid called from its subclass added a underscore in each method.
96
+
97
+ # Returns a record hash with the column names as keys and column values
98
+ # as values.
99
+ def _select_one(arel, name = nil, binds = [])
100
+ result = select(arel)
101
+ result.first if result
102
+ end
103
+
104
+ # Returns a single value from a record
105
+ def _select_value(arel, name = nil, binds = [])
106
+ if result = _select_one(arel)
107
+ result.values.first
108
+ end
109
+ end
110
+ end
111
+
112
+ # Returns array with major and minor version of database (e.g. [12, 1])
113
+ def database_version
114
+ raise NoMethodError, "Not implemented for this raw driver"
115
+ end
116
+ class ConnectionException < StandardError #:nodoc:
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # if MRI or YARV
123
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
124
+ ORACLE_ENHANCED_CONNECTION = :oci
125
+ require "active_record/connection_adapters/oracle_enhanced/oci_connection"
126
+ # if JRuby
127
+ elsif RUBY_ENGINE == "jruby"
128
+ ORACLE_ENHANCED_CONNECTION = :jdbc
129
+ require "active_record/connection_adapters/oracle_enhanced/jdbc_connection"
130
+ else
131
+ raise "Unsupported Ruby engine #{RUBY_ENGINE}"
132
+ end
@@ -0,0 +1,345 @@
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)}".dup
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
+
146
+ def create_datastore_procedure(table_name, procedure_name, column_names, options)
147
+ quoted_table_name = quote_table_name(table_name)
148
+ select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i }
149
+ select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, " ") }
150
+ keys, selected_columns = parse_select_queries(select_queries)
151
+ quoted_column_names = (column_names + keys).map { |col| quote_column_name(col) }
152
+ execute <<-SQL
153
+ CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
154
+ (p_rowid IN ROWID,
155
+ p_clob IN OUT NOCOPY CLOB) IS
156
+ -- add_context_index_parameters #{(column_names + select_queries).inspect}#{!options.empty? ? ', '.dup << options.inspect[1..-2] : ''}
157
+ #{
158
+ selected_columns.map do |cols|
159
+ cols.map do |col|
160
+ raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
161
+ "l_#{col} VARCHAR2(32767);\n"
162
+ end.join
163
+ end.join
164
+ } BEGIN
165
+ FOR r1 IN (
166
+ SELECT #{quoted_column_names.join(', ')}
167
+ FROM #{quoted_table_name}
168
+ WHERE #{quoted_table_name}.ROWID = p_rowid
169
+ ) LOOP
170
+ #{
171
+ (column_names.map do |col|
172
+ col = col.to_s
173
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 2}, '<#{col}>');\n".dup <<
174
+ "IF LENGTH(r1.#{col}) > 0 THEN\n" <<
175
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
176
+ "END IF;\n" <<
177
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 3}, '</#{col}>');\n"
178
+ end.join) <<
179
+ (selected_columns.zip(select_queries).map do |cols, query|
180
+ (cols.map do |col|
181
+ "l_#{col} := '';\n"
182
+ end.join) <<
183
+ "FOR r2 IN (\n" <<
184
+ query.gsub(/:(\w+)/, "r1.\\1") << "\n) LOOP\n" <<
185
+ (cols.map do |col|
186
+ "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
187
+ end.join) <<
188
+ "END LOOP;\n" <<
189
+ (cols.map do |col|
190
+ col = col.to_s
191
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 2}, '<#{col}>');\n".dup <<
192
+ "IF LENGTH(l_#{col}) > 0 THEN\n" <<
193
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
194
+ "END IF;\n" <<
195
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length + 3}, '</#{col}>');\n"
196
+ end.join)
197
+ end.join)
198
+ }
199
+ END LOOP;
200
+ END;
201
+ SQL
202
+ end
203
+
204
+ def parse_select_queries(select_queries)
205
+ keys = []
206
+ selected_columns = []
207
+ select_queries.each do |query|
208
+ # get primary or foreign keys like :id or :something_id
209
+ keys << (query.scan(/:\w+/).map { |k| k[1..-1].downcase.to_sym })
210
+ select_part = query.scan(/^select\s.*\sfrom/i).first
211
+ selected_columns << select_part.scan(/\sas\s+(\w+)/i).map { |c| c.first }
212
+ end
213
+ [keys.flatten.uniq, selected_columns]
214
+ end
215
+
216
+ def create_datastore_preference(datastore_name, procedure_name)
217
+ drop_ctx_preference(datastore_name)
218
+ execute <<-SQL
219
+ BEGIN
220
+ CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
221
+ CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
222
+ END;
223
+ SQL
224
+ end
225
+
226
+ def create_storage_preference(storage_name, tablespace)
227
+ drop_ctx_preference(storage_name)
228
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n".dup
229
+ ["I_TABLE_CLAUSE", "K_TABLE_CLAUSE", "R_TABLE_CLAUSE",
230
+ "N_TABLE_CLAUSE", "I_INDEX_CLAUSE", "P_TABLE_CLAUSE"].each do |clause|
231
+ default_clause = case clause
232
+ when "R_TABLE_CLAUSE"; "LOB(DATA) STORE AS (CACHE) "
233
+ when "I_INDEX_CLAUSE"; "COMPRESS 2 "
234
+ else ""
235
+ end
236
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
237
+ end
238
+ sql << "END;\n"
239
+ execute sql
240
+ end
241
+
242
+ def create_lexer_preference(lexer_name, lexer_type, options)
243
+ drop_ctx_preference(lexer_name)
244
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n".dup
245
+ options.each do |key, value|
246
+ plsql_value = case value
247
+ when String; "'#{value}'"
248
+ when true; "'YES'"
249
+ when false; "'NO'"
250
+ when nil; "NULL"
251
+ else value
252
+ end
253
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
254
+ end
255
+ sql << "END;\n"
256
+ execute sql
257
+ end
258
+
259
+ def create_wordlist_preference(wordlist_name, wordlist_type, options)
260
+ drop_ctx_preference(wordlist_name)
261
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n".dup
262
+ options.each do |key, value|
263
+ plsql_value = case value
264
+ when String; "'#{value}'"
265
+ when true; "'YES'"
266
+ when false; "'NO'"
267
+ when nil; "NULL"
268
+ else value
269
+ end
270
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n"
271
+ end
272
+ sql << "END;\n"
273
+ execute sql
274
+ end
275
+
276
+ def drop_ctx_preference(preference_name)
277
+ execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
278
+ end
279
+
280
+ def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
281
+ trigger_name = default_index_column_trigger_name(index_name)
282
+ columns = Array(index_column_source)
283
+ quoted_column_names = columns.map { |col| quote_column_name(col) }.join(", ")
284
+ execute <<-SQL
285
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
286
+ BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
287
+ BEGIN
288
+ :new.#{quote_column_name(index_column)} := '1';
289
+ END;
290
+ SQL
291
+ end
292
+
293
+ def drop_index_column_trigger(index_name)
294
+ trigger_name = default_index_column_trigger_name(index_name)
295
+ execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
296
+ end
297
+
298
+ def default_datastore_procedure(index_name)
299
+ "#{index_name}_prc"
300
+ end
301
+
302
+ def default_datastore_name(index_name)
303
+ "#{index_name}_dst"
304
+ end
305
+
306
+ def default_storage_name(index_name)
307
+ "#{index_name}_sto"
308
+ end
309
+
310
+ def default_index_column_trigger_name(index_name)
311
+ "#{index_name}_trg"
312
+ end
313
+
314
+ def default_lexer_name(index_name)
315
+ "#{index_name}_lex"
316
+ end
317
+
318
+ def default_wordlist_name(index_name)
319
+ "#{index_name}_wl"
320
+ end
321
+
322
+ module BaseClassMethods
323
+ # Declare that model table has context index defined.
324
+ # As a result <tt>contains</tt> class scope method is defined.
325
+ def has_context_index
326
+ extend ContextIndexClassMethods
327
+ end
328
+ end
329
+
330
+ module ContextIndexClassMethods
331
+ # Add context index condition.
332
+ def contains(column, query, options = {})
333
+ score_label = options[:label].to_i || 1
334
+ where("CONTAINS(#{connection.quote_table_name(column)}, ?, #{score_label}) > 0", query).
335
+ order(Arel.sql("SCORE(#{score_label}) DESC"))
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+
343
+ ActiveRecord::Base.class_eval do
344
+ extend ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex::BaseClassMethods
345
+ end