activerecord-oracle_enhanced-adapter 1.5.6 → 1.6.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis/oracle/download.sh +14 -0
  3. data/.travis/oracle/install.sh +31 -0
  4. data/.travis/setup_accounts.sh +9 -0
  5. data/.travis.yml +39 -0
  6. data/Gemfile +8 -8
  7. data/History.md +189 -0
  8. data/README.md +388 -178
  9. data/RUNNING_TESTS.md +11 -6
  10. data/VERSION +1 -1
  11. data/activerecord-oracle_enhanced-adapter.gemspec +29 -26
  12. data/lib/active_record/connection_adapters/{oracle_enhanced_column.rb → oracle_enhanced/column.rb} +14 -63
  13. data/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb +66 -0
  14. data/lib/active_record/connection_adapters/{oracle_enhanced_connection.rb → oracle_enhanced/connection.rb} +2 -2
  15. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +347 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +260 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb +40 -0
  18. data/lib/active_record/connection_adapters/{oracle_enhanced_jdbc_connection.rb → oracle_enhanced/jdbc_connection.rb} +13 -4
  19. data/lib/active_record/connection_adapters/{oracle_enhanced_oci_connection.rb → oracle_enhanced/oci_connection.rb} +11 -5
  20. data/lib/active_record/connection_adapters/{oracle_enhanced_procedures.rb → oracle_enhanced/procedures.rb} +1 -1
  21. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_creation.rb → oracle_enhanced/schema_creation.rb} +34 -35
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +95 -0
  23. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_dumper.rb → oracle_enhanced/schema_dumper.rb} +14 -37
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +562 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +65 -0
  26. data/lib/active_record/connection_adapters/{oracle_enhanced_structure_dump.rb → oracle_enhanced/structure_dump.rb} +63 -14
  27. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +1 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +171 -73
  29. data/lib/active_record/oracle_enhanced/type/integer.rb +13 -0
  30. data/lib/active_record/oracle_enhanced/type/raw.rb +13 -0
  31. data/lib/active_record/oracle_enhanced/type/timestamp.rb +11 -0
  32. data/lib/activerecord-oracle_enhanced-adapter.rb +1 -1
  33. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +127 -49
  34. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +46 -5
  35. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +11 -3
  36. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +3 -3
  37. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +151 -78
  38. data/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +4 -4
  39. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +10 -16
  40. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +1 -1
  41. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +5 -5
  42. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +65 -181
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +114 -11
  44. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +17 -1
  45. data/spec/spec_config.yaml.template +11 -0
  46. data/spec/spec_helper.rb +31 -12
  47. data/spec/support/alter_system_user_password.sql +2 -0
  48. data/spec/support/create_oracle_enhanced_users.sql +31 -0
  49. metadata +37 -27
  50. data/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb +0 -77
  51. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +0 -350
  52. data/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb +0 -262
  53. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +0 -45
  54. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +0 -197
  55. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +0 -450
  56. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +0 -258
  57. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +0 -1
  58. /data/lib/active_record/connection_adapters/{oracle_enhanced_cpk.rb → oracle_enhanced/cpk.rb} +0 -0
  59. /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
- if database && !host && !config[:url] && ENV['TNS_ADMIN']
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
- # default schema owner
163
- @owner = username.upcase unless username.nil?
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