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.
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