activerecord-oracle_enhanced-adapter 1.5.6 → 1.6.9
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/.travis/oracle/download.sh +14 -0
- data/.travis/oracle/install.sh +31 -0
- data/.travis/setup_accounts.sh +9 -0
- data/.travis.yml +39 -0
- data/Gemfile +8 -8
- data/History.md +189 -0
- data/README.md +388 -178
- data/RUNNING_TESTS.md +11 -6
- data/VERSION +1 -1
- data/activerecord-oracle_enhanced-adapter.gemspec +29 -26
- data/lib/active_record/connection_adapters/{oracle_enhanced_column.rb → oracle_enhanced/column.rb} +14 -63
- data/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb +66 -0
- data/lib/active_record/connection_adapters/{oracle_enhanced_connection.rb → oracle_enhanced/connection.rb} +2 -2
- data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +347 -0
- data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +260 -0
- data/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb +40 -0
- data/lib/active_record/connection_adapters/{oracle_enhanced_jdbc_connection.rb → oracle_enhanced/jdbc_connection.rb} +13 -4
- data/lib/active_record/connection_adapters/{oracle_enhanced_oci_connection.rb → oracle_enhanced/oci_connection.rb} +11 -5
- data/lib/active_record/connection_adapters/{oracle_enhanced_procedures.rb → oracle_enhanced/procedures.rb} +1 -1
- data/lib/active_record/connection_adapters/{oracle_enhanced_schema_creation.rb → oracle_enhanced/schema_creation.rb} +34 -35
- data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +95 -0
- data/lib/active_record/connection_adapters/{oracle_enhanced_schema_dumper.rb → oracle_enhanced/schema_dumper.rb} +14 -37
- data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +562 -0
- data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +65 -0
- data/lib/active_record/connection_adapters/{oracle_enhanced_structure_dump.rb → oracle_enhanced/structure_dump.rb} +63 -14
- data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +1 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +171 -73
- data/lib/active_record/oracle_enhanced/type/integer.rb +13 -0
- data/lib/active_record/oracle_enhanced/type/raw.rb +13 -0
- data/lib/active_record/oracle_enhanced/type/timestamp.rb +11 -0
- data/lib/activerecord-oracle_enhanced-adapter.rb +1 -1
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +127 -49
- data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +46 -5
- data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +11 -3
- data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +3 -3
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +151 -78
- data/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +4 -4
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +10 -16
- data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +1 -1
- data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +5 -5
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +65 -181
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +114 -11
- data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +17 -1
- data/spec/spec_config.yaml.template +11 -0
- data/spec/spec_helper.rb +31 -12
- data/spec/support/alter_system_user_password.sql +2 -0
- data/spec/support/create_oracle_enhanced_users.sql +31 -0
- metadata +37 -27
- data/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb +0 -77
- data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +0 -350
- data/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb +0 -262
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +0 -45
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +0 -197
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +0 -450
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +0 -258
- data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +0 -1
- /data/lib/active_record/connection_adapters/{oracle_enhanced_cpk.rb → oracle_enhanced/cpk.rb} +0 -0
- /data/lib/active_record/connection_adapters/{oracle_enhanced_database_tasks.rb → oracle_enhanced/database_tasks.rb} +0 -0
@@ -0,0 +1,347 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module OracleEnhanced
|
4
|
+
module ContextIndex
|
5
|
+
|
6
|
+
# Define full text index with Oracle specific CONTEXT index type
|
7
|
+
#
|
8
|
+
# Oracle CONTEXT index by default supports full text indexing of one column.
|
9
|
+
# This method allows full text index creation also on several columns
|
10
|
+
# as well as indexing related table columns by generating stored procedure
|
11
|
+
# that concatenates all columns for indexing as well as generating trigger
|
12
|
+
# that will update main index column to trigger reindexing of record.
|
13
|
+
#
|
14
|
+
# Use +contains+ ActiveRecord model instance method to add CONTAINS where condition
|
15
|
+
# and order by score of matched results.
|
16
|
+
#
|
17
|
+
# Options:
|
18
|
+
#
|
19
|
+
# * <tt>:name</tt>
|
20
|
+
# * <tt>:index_column</tt>
|
21
|
+
# * <tt>:index_column_trigger_on</tt>
|
22
|
+
# * <tt>:tablespace</tt>
|
23
|
+
# * <tt>:sync</tt> - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL').
|
24
|
+
# * <tt>:lexer</tt> - Lexer options (e.g. <tt>:type => 'BASIC_LEXER', :base_letter => true</tt>).
|
25
|
+
# * <tt>:wordlist</tt> - Wordlist options (e.g. <tt>:type => 'BASIC_WORDLIST', :prefix_index => true</tt>).
|
26
|
+
# * <tt>:transactional</tt> - When +true+, the CONTAINS operator will process inserted and updated rows.
|
27
|
+
#
|
28
|
+
# ===== Examples
|
29
|
+
#
|
30
|
+
# ====== Creating single column index
|
31
|
+
# add_context_index :posts, :title
|
32
|
+
# search with
|
33
|
+
# Post.contains(:title, 'word')
|
34
|
+
#
|
35
|
+
# ====== Creating index on several columns
|
36
|
+
# add_context_index :posts, [:title, :body]
|
37
|
+
# search with (use first column as argument for contains method but it will search in all index columns)
|
38
|
+
# Post.contains(:title, 'word')
|
39
|
+
#
|
40
|
+
# ====== Creating index on several columns with dummy index column and commit option
|
41
|
+
# add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
|
42
|
+
# search with
|
43
|
+
# Post.contains(:all_text, 'word')
|
44
|
+
#
|
45
|
+
# ====== Creating index with trigger option (will reindex when specified columns are updated)
|
46
|
+
# add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
|
47
|
+
# :index_column_trigger_on => [:created_at, :updated_at]
|
48
|
+
# search with
|
49
|
+
# Post.contains(:all_text, 'word')
|
50
|
+
#
|
51
|
+
# ====== Creating index on multiple tables
|
52
|
+
# add_context_index :posts,
|
53
|
+
# [:title, :body,
|
54
|
+
# # specify aliases always with AS keyword
|
55
|
+
# "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
|
56
|
+
# ],
|
57
|
+
# :name => 'post_and_comments_index',
|
58
|
+
# :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
|
59
|
+
# :sync => 'ON COMMIT'
|
60
|
+
# search in any table columns
|
61
|
+
# Post.contains(:all_text, 'word')
|
62
|
+
# search in specified column
|
63
|
+
# Post.contains(:all_text, "aaa within title")
|
64
|
+
# Post.contains(:all_text, "bbb within comment_author")
|
65
|
+
#
|
66
|
+
# ====== Creating index using lexer
|
67
|
+
# add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... }
|
68
|
+
#
|
69
|
+
# ====== Creating index using wordlist
|
70
|
+
# add_context_index :posts, :title, :wordlist => { :type => 'BASIC_WORDLIST', :prefix_index => true, ... }
|
71
|
+
#
|
72
|
+
# ====== Creating transactional index (will reindex changed rows when querying)
|
73
|
+
# add_context_index :posts, :title, :transactional => true
|
74
|
+
#
|
75
|
+
def add_context_index(table_name, column_name, options = {})
|
76
|
+
self.all_schema_indexes = nil
|
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
|
+
self.all_schema_indexes = nil
|
132
|
+
unless Hash === options # if column names passed as argument
|
133
|
+
options = {:column => Array(options)}
|
134
|
+
end
|
135
|
+
index_name = options[:name] || index_name(table_name,
|
136
|
+
:column => options[:index_column] || options[:column], :identifier_max_length => 25)
|
137
|
+
execute "DROP INDEX #{index_name}"
|
138
|
+
drop_ctx_preference(default_datastore_name(index_name))
|
139
|
+
drop_ctx_preference(default_storage_name(index_name))
|
140
|
+
procedure_name = default_datastore_procedure(index_name)
|
141
|
+
execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
|
142
|
+
drop_index_column_trigger(index_name)
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def create_datastore_procedure(table_name, procedure_name, column_names, options)
|
148
|
+
quoted_table_name = quote_table_name(table_name)
|
149
|
+
select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i }
|
150
|
+
select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') }
|
151
|
+
keys, selected_columns = parse_select_queries(select_queries)
|
152
|
+
quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)}
|
153
|
+
execute compress_lines(<<-SQL)
|
154
|
+
CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
|
155
|
+
(p_rowid IN ROWID,
|
156
|
+
p_clob IN OUT NOCOPY CLOB) IS
|
157
|
+
-- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''}
|
158
|
+
#{
|
159
|
+
selected_columns.map do |cols|
|
160
|
+
cols.map do |col|
|
161
|
+
raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
|
162
|
+
"l_#{col} VARCHAR2(32767);\n"
|
163
|
+
end.join
|
164
|
+
end.join
|
165
|
+
} BEGIN
|
166
|
+
FOR r1 IN (
|
167
|
+
SELECT #{quoted_column_names.join(', ')}
|
168
|
+
FROM #{quoted_table_name}
|
169
|
+
WHERE #{quoted_table_name}.ROWID = p_rowid
|
170
|
+
) LOOP
|
171
|
+
#{
|
172
|
+
(column_names.map do |col|
|
173
|
+
col = col.to_s
|
174
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
|
175
|
+
"IF LENGTH(r1.#{col}) > 0 THEN\n" <<
|
176
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
|
177
|
+
"END IF;\n" <<
|
178
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
|
179
|
+
end.join) <<
|
180
|
+
(selected_columns.zip(select_queries).map do |cols, query|
|
181
|
+
(cols.map do |col|
|
182
|
+
"l_#{col} := '';\n"
|
183
|
+
end.join) <<
|
184
|
+
"FOR r2 IN (\n" <<
|
185
|
+
query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" <<
|
186
|
+
(cols.map do |col|
|
187
|
+
"l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
|
188
|
+
end.join) <<
|
189
|
+
"END LOOP;\n" <<
|
190
|
+
(cols.map do |col|
|
191
|
+
col = col.to_s
|
192
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
|
193
|
+
"IF LENGTH(l_#{col}) > 0 THEN\n" <<
|
194
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
|
195
|
+
"END IF;\n" <<
|
196
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
|
197
|
+
end.join)
|
198
|
+
end.join)
|
199
|
+
}
|
200
|
+
END LOOP;
|
201
|
+
END;
|
202
|
+
SQL
|
203
|
+
end
|
204
|
+
|
205
|
+
def parse_select_queries(select_queries)
|
206
|
+
keys = []
|
207
|
+
selected_columns = []
|
208
|
+
select_queries.each do |query|
|
209
|
+
# get primary or foreign keys like :id or :something_id
|
210
|
+
keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym})
|
211
|
+
select_part = query.scan(/^select\s.*\sfrom/i).first
|
212
|
+
selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first}
|
213
|
+
end
|
214
|
+
[keys.flatten.uniq, selected_columns]
|
215
|
+
end
|
216
|
+
|
217
|
+
def create_datastore_preference(datastore_name, procedure_name)
|
218
|
+
drop_ctx_preference(datastore_name)
|
219
|
+
execute <<-SQL
|
220
|
+
BEGIN
|
221
|
+
CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
|
222
|
+
CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
|
223
|
+
END;
|
224
|
+
SQL
|
225
|
+
end
|
226
|
+
|
227
|
+
def create_storage_preference(storage_name, tablespace)
|
228
|
+
drop_ctx_preference(storage_name)
|
229
|
+
sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
|
230
|
+
['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE',
|
231
|
+
'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
|
232
|
+
default_clause = case clause
|
233
|
+
when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) '
|
234
|
+
when 'I_INDEX_CLAUSE'; 'COMPRESS 2 '
|
235
|
+
else ''
|
236
|
+
end
|
237
|
+
sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
|
238
|
+
end
|
239
|
+
sql << "END;\n"
|
240
|
+
execute sql
|
241
|
+
end
|
242
|
+
|
243
|
+
def create_lexer_preference(lexer_name, lexer_type, options)
|
244
|
+
drop_ctx_preference(lexer_name)
|
245
|
+
sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n"
|
246
|
+
options.each do |key, value|
|
247
|
+
plsql_value = case value
|
248
|
+
when String; "'#{value}'"
|
249
|
+
when true; "'YES'"
|
250
|
+
when false; "'NO'"
|
251
|
+
when nil; 'NULL'
|
252
|
+
else value
|
253
|
+
end
|
254
|
+
sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
|
255
|
+
end
|
256
|
+
sql << "END;\n"
|
257
|
+
execute sql
|
258
|
+
end
|
259
|
+
|
260
|
+
def create_wordlist_preference(wordlist_name, wordlist_type, options)
|
261
|
+
drop_ctx_preference(wordlist_name)
|
262
|
+
sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n"
|
263
|
+
options.each do |key, value|
|
264
|
+
plsql_value = case value
|
265
|
+
when String; "'#{value}'"
|
266
|
+
when true; "'YES'"
|
267
|
+
when false; "'NO'"
|
268
|
+
when nil; 'NULL'
|
269
|
+
else value
|
270
|
+
end
|
271
|
+
sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n"
|
272
|
+
end
|
273
|
+
sql << "END;\n"
|
274
|
+
execute sql
|
275
|
+
end
|
276
|
+
|
277
|
+
def drop_ctx_preference(preference_name)
|
278
|
+
execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
|
282
|
+
trigger_name = default_index_column_trigger_name(index_name)
|
283
|
+
columns = Array(index_column_source)
|
284
|
+
quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ')
|
285
|
+
execute compress_lines(<<-SQL)
|
286
|
+
CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
|
287
|
+
BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
|
288
|
+
BEGIN
|
289
|
+
:new.#{quote_column_name(index_column)} := '1';
|
290
|
+
END;
|
291
|
+
SQL
|
292
|
+
end
|
293
|
+
|
294
|
+
def drop_index_column_trigger(index_name)
|
295
|
+
trigger_name = default_index_column_trigger_name(index_name)
|
296
|
+
execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
|
297
|
+
end
|
298
|
+
|
299
|
+
def default_datastore_procedure(index_name)
|
300
|
+
"#{index_name}_prc"
|
301
|
+
end
|
302
|
+
|
303
|
+
def default_datastore_name(index_name)
|
304
|
+
"#{index_name}_dst"
|
305
|
+
end
|
306
|
+
|
307
|
+
def default_storage_name(index_name)
|
308
|
+
"#{index_name}_sto"
|
309
|
+
end
|
310
|
+
|
311
|
+
def default_index_column_trigger_name(index_name)
|
312
|
+
"#{index_name}_trg"
|
313
|
+
end
|
314
|
+
|
315
|
+
def default_lexer_name(index_name)
|
316
|
+
"#{index_name}_lex"
|
317
|
+
end
|
318
|
+
|
319
|
+
def default_wordlist_name(index_name)
|
320
|
+
"#{index_name}_wl"
|
321
|
+
end
|
322
|
+
|
323
|
+
module BaseClassMethods
|
324
|
+
# Declare that model table has context index defined.
|
325
|
+
# As a result <tt>contains</tt> class scope method is defined.
|
326
|
+
def has_context_index
|
327
|
+
extend ContextIndexClassMethods
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
module ContextIndexClassMethods
|
332
|
+
# Add context index condition.
|
333
|
+
def contains(column, query, options ={})
|
334
|
+
score_label = options[:label].to_i || 1
|
335
|
+
where("CONTAINS(#{connection.quote_table_name(column)}, ?, #{score_label}) > 0", query).
|
336
|
+
order("SCORE(#{score_label}) DESC")
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
ActiveRecord::Base.class_eval do
|
346
|
+
extend ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex::BaseClassMethods
|
347
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module OracleEnhanced
|
4
|
+
module DatabaseStatements
|
5
|
+
# DATABASE STATEMENTS ======================================
|
6
|
+
#
|
7
|
+
# see: abstract/database_statements.rb
|
8
|
+
|
9
|
+
# Executes a SQL statement
|
10
|
+
def execute(sql, name = nil)
|
11
|
+
log(sql, name) { @connection.exec(sql) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear_cache!
|
15
|
+
@statements.clear
|
16
|
+
reload_type_map
|
17
|
+
end
|
18
|
+
|
19
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
20
|
+
type_casted_binds = binds.map { |col, val|
|
21
|
+
[col, type_cast(val, col)]
|
22
|
+
}
|
23
|
+
log(sql, name, type_casted_binds) do
|
24
|
+
cursor = nil
|
25
|
+
cached = false
|
26
|
+
if without_prepared_statement?(binds)
|
27
|
+
cursor = @connection.prepare(sql)
|
28
|
+
else
|
29
|
+
unless @statements.key? sql
|
30
|
+
@statements[sql] = @connection.prepare(sql)
|
31
|
+
end
|
32
|
+
|
33
|
+
cursor = @statements[sql]
|
34
|
+
|
35
|
+
type_casted_binds.each_with_index do |bind, i|
|
36
|
+
col, val = bind
|
37
|
+
cursor.bind_param(i + 1, val, col)
|
38
|
+
end
|
39
|
+
|
40
|
+
cached = true
|
41
|
+
end
|
42
|
+
|
43
|
+
cursor.exec
|
44
|
+
|
45
|
+
if name == 'EXPLAIN' and sql =~ /^EXPLAIN/
|
46
|
+
res = true
|
47
|
+
else
|
48
|
+
columns = cursor.get_col_names.map do |col_name|
|
49
|
+
@connection.oracle_downcase(col_name)
|
50
|
+
end
|
51
|
+
rows = []
|
52
|
+
fetch_options = {:get_lob_value => (name != 'Writable Large Object')}
|
53
|
+
while row = cursor.fetch(fetch_options)
|
54
|
+
rows << row
|
55
|
+
end
|
56
|
+
res = ActiveRecord::Result.new(columns, rows)
|
57
|
+
end
|
58
|
+
|
59
|
+
cursor.close unless cached
|
60
|
+
res
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def supports_statement_cache?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def supports_explain?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def explain(arel, binds = [])
|
73
|
+
sql = "EXPLAIN PLAN FOR #{to_sql(arel, binds)}"
|
74
|
+
return if sql =~ /FROM all_/
|
75
|
+
if ORACLE_ENHANCED_CONNECTION == :jdbc
|
76
|
+
exec_query(sql, 'EXPLAIN', binds)
|
77
|
+
else
|
78
|
+
exec_query(sql, 'EXPLAIN')
|
79
|
+
end
|
80
|
+
select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns an array of arrays containing the field values.
|
84
|
+
# Order is the same as that returned by #columns.
|
85
|
+
def select_rows(sql, name = nil, binds = [])
|
86
|
+
exec_query(sql, name, binds).rows
|
87
|
+
end
|
88
|
+
|
89
|
+
# Executes an INSERT statement and returns the new record's ID
|
90
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
91
|
+
# if primary key value is already prefetched from sequence
|
92
|
+
# or if there is no primary key
|
93
|
+
if id_value || pk.nil?
|
94
|
+
execute(sql, name)
|
95
|
+
return id_value
|
96
|
+
end
|
97
|
+
|
98
|
+
sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk))
|
99
|
+
log(sql, name) do
|
100
|
+
@connection.exec_with_returning(sql_with_returning)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
protected :insert_sql
|
104
|
+
|
105
|
+
# New method in ActiveRecord 3.1
|
106
|
+
# Will add RETURNING clause in case of trigger generated primary keys
|
107
|
+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
|
108
|
+
unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys))
|
109
|
+
sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id"
|
110
|
+
returning_id_col = new_column("returning_id", nil, Type::Value.new, "number", true, "dual", true, true)
|
111
|
+
(binds = binds.dup) << [returning_id_col, nil]
|
112
|
+
end
|
113
|
+
[sql, binds]
|
114
|
+
end
|
115
|
+
|
116
|
+
# New method in ActiveRecord 3.1
|
117
|
+
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
|
118
|
+
type_casted_binds = binds.map { |col, val|
|
119
|
+
[col, type_cast(val, col)]
|
120
|
+
}
|
121
|
+
log(sql, name, type_casted_binds) do
|
122
|
+
returning_id_col = returning_id_index = nil
|
123
|
+
if without_prepared_statement?(binds)
|
124
|
+
cursor = @connection.prepare(sql)
|
125
|
+
else
|
126
|
+
unless @statements.key?(sql)
|
127
|
+
@statements[sql] = @connection.prepare(sql)
|
128
|
+
end
|
129
|
+
|
130
|
+
cursor = @statements[sql]
|
131
|
+
|
132
|
+
type_casted_binds.each_with_index do |bind, i|
|
133
|
+
col, val = bind
|
134
|
+
if col.returning_id?
|
135
|
+
returning_id_col = [col]
|
136
|
+
returning_id_index = i + 1
|
137
|
+
cursor.bind_returning_param(returning_id_index, Integer)
|
138
|
+
else
|
139
|
+
cursor.bind_param(i + 1, val, col)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
cursor.exec_update
|
145
|
+
|
146
|
+
rows = []
|
147
|
+
if returning_id_index
|
148
|
+
returning_id = cursor.get_returning_param(returning_id_index, Integer)
|
149
|
+
rows << [returning_id]
|
150
|
+
end
|
151
|
+
ActiveRecord::Result.new(returning_id_col || [], rows)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# New method in ActiveRecord 3.1
|
156
|
+
def exec_update(sql, name, binds)
|
157
|
+
type_casted_binds = binds.map { |col, val|
|
158
|
+
[col, type_cast(val, col)]
|
159
|
+
}
|
160
|
+
log(sql, name, type_casted_binds) do
|
161
|
+
cached = false
|
162
|
+
if without_prepared_statement?(binds)
|
163
|
+
cursor = @connection.prepare(sql)
|
164
|
+
else
|
165
|
+
cursor = if @statements.key?(sql)
|
166
|
+
@statements[sql]
|
167
|
+
else
|
168
|
+
@statements[sql] = @connection.prepare(sql)
|
169
|
+
end
|
170
|
+
|
171
|
+
type_casted_binds.each_with_index do |bind, i|
|
172
|
+
col, val = bind
|
173
|
+
cursor.bind_param(i + 1, val, col)
|
174
|
+
end
|
175
|
+
cached = true
|
176
|
+
end
|
177
|
+
|
178
|
+
res = cursor.exec_update
|
179
|
+
cursor.close unless cached
|
180
|
+
res
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
alias :exec_delete :exec_update
|
185
|
+
|
186
|
+
def begin_db_transaction #:nodoc:
|
187
|
+
@connection.autocommit = false
|
188
|
+
end
|
189
|
+
|
190
|
+
def transaction_isolation_levels
|
191
|
+
# Oracle database supports `READ COMMITTED` and `SERIALIZABLE`
|
192
|
+
# No read uncommitted nor repeatable read supppoted
|
193
|
+
# http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_10005.htm#SQLRF55422
|
194
|
+
{
|
195
|
+
read_committed: "READ COMMITTED",
|
196
|
+
serializable: "SERIALIZABLE"
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
def begin_isolated_db_transaction(isolation)
|
201
|
+
begin_db_transaction
|
202
|
+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def commit_db_transaction #:nodoc:
|
206
|
+
@connection.commit
|
207
|
+
ensure
|
208
|
+
@connection.autocommit = true
|
209
|
+
end
|
210
|
+
|
211
|
+
def exec_rollback_db_transaction #:nodoc:
|
212
|
+
@connection.rollback
|
213
|
+
ensure
|
214
|
+
@connection.autocommit = true
|
215
|
+
end
|
216
|
+
|
217
|
+
def create_savepoint(name = current_savepoint_name) #:nodoc:
|
218
|
+
execute("SAVEPOINT #{name}")
|
219
|
+
end
|
220
|
+
|
221
|
+
def exec_rollback_to_savepoint(name = current_savepoint_name) #:nodoc:
|
222
|
+
execute("ROLLBACK TO #{name}")
|
223
|
+
end
|
224
|
+
|
225
|
+
def release_savepoint(name = current_savepoint_name) #:nodoc:
|
226
|
+
# there is no RELEASE SAVEPOINT statement in Oracle
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns default sequence name for table.
|
230
|
+
# Will take all or first 26 characters of table name and append _seq suffix
|
231
|
+
def default_sequence_name(table_name, primary_key = nil)
|
232
|
+
table_name.to_s.gsub((/(^|\.)([\w$-]{1,#{sequence_name_length-4}})([\w$-]*)$/), '\1\2_seq')
|
233
|
+
end
|
234
|
+
|
235
|
+
# Inserts the given fixture into the table. Overridden to properly handle lobs.
|
236
|
+
def insert_fixture(fixture, table_name) #:nodoc:
|
237
|
+
super
|
238
|
+
|
239
|
+
if ActiveRecord::Base.pluralize_table_names
|
240
|
+
klass = table_name.to_s.singularize.camelize
|
241
|
+
else
|
242
|
+
klass = table_name.to_s.camelize
|
243
|
+
end
|
244
|
+
|
245
|
+
klass = klass.constantize rescue nil
|
246
|
+
if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
|
247
|
+
write_lobs(table_name, klass, fixture, klass.lob_columns)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
def select(sql, name = nil, binds = [])
|
254
|
+
exec_query(sql, name, binds)
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module ConnectionAdapters #:nodoc:
|
3
|
+
module OracleEnhancedDirty #:nodoc:
|
4
|
+
|
5
|
+
module InstanceMethods #:nodoc:
|
6
|
+
private
|
7
|
+
|
8
|
+
def _field_changed?(attr, old_value)
|
9
|
+
new_value = read_attribute(attr)
|
10
|
+
raw_value = read_attribute_before_type_cast(attr)
|
11
|
+
|
12
|
+
if self.class.columns_hash.include?(attr.to_s)
|
13
|
+
column = column_for_attribute(attr)
|
14
|
+
|
15
|
+
# Oracle stores empty string '' as NULL
|
16
|
+
# therefore need to convert empty string value to nil if old value is nil
|
17
|
+
if column.type == :string && column.null && old_value.nil?
|
18
|
+
new_value = nil if new_value == ''
|
19
|
+
end
|
20
|
+
column.changed?(old_value, new_value, raw_value)
|
21
|
+
else
|
22
|
+
new_value != old_value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def non_zero?(value)
|
27
|
+
value !~ /\A0+(\.0+)?\z/
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if ActiveRecord::Base.method_defined?(:changed?)
|
37
|
+
ActiveRecord::Base.class_eval do
|
38
|
+
include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods
|
39
|
+
end
|
40
|
+
end
|
@@ -109,8 +109,9 @@ module ActiveRecord
|
|
109
109
|
host, port = config[:host], config[:port]
|
110
110
|
privilege = config[:privilege] && config[:privilege].to_s
|
111
111
|
|
112
|
-
# connection using TNS alias
|
113
|
-
|
112
|
+
# connection using TNS alias, or connection-string from DATABASE_URL
|
113
|
+
using_tns_alias = !host && !config[:url] && ENV['TNS_ADMIN']
|
114
|
+
if database && (using_tns_alias || host == 'connection-string')
|
114
115
|
url = "jdbc:oracle:thin:@#{database}"
|
115
116
|
else
|
116
117
|
unless database.match(/^(\:|\/)/)
|
@@ -159,8 +160,14 @@ module ActiveRecord
|
|
159
160
|
|
160
161
|
self.autocommit = true
|
161
162
|
|
162
|
-
|
163
|
-
|
163
|
+
schema = config[:schema] && config[:schema].to_s
|
164
|
+
if schema.blank?
|
165
|
+
# default schema owner
|
166
|
+
@owner = username.upcase unless username.nil?
|
167
|
+
else
|
168
|
+
exec "alter session set current_schema = #{schema}"
|
169
|
+
@owner = schema
|
170
|
+
end
|
164
171
|
|
165
172
|
@raw_connection
|
166
173
|
end
|
@@ -525,6 +532,8 @@ module ActiveRecord
|
|
525
532
|
else
|
526
533
|
BigDecimal.new(d.stringValue)
|
527
534
|
end
|
535
|
+
when :BINARY_FLOAT
|
536
|
+
rset.getFloat(i)
|
528
537
|
when :VARCHAR2, :CHAR, :LONG, :NVARCHAR2, :NCHAR
|
529
538
|
rset.getString(i)
|
530
539
|
when :DATE
|