activerecord-oracle_enhanced-adapter 1.2.4 → 1.3.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 (33) hide show
  1. data/.gitignore +0 -1
  2. data/History.txt +20 -0
  3. data/README.rdoc +7 -3
  4. data/Rakefile +1 -2
  5. data/VERSION +1 -1
  6. data/activerecord-oracle_enhanced-adapter.gemspec +96 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced.rake +11 -8
  8. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +37 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +317 -180
  10. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +282 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +3 -2
  12. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +1 -1
  13. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +3 -3
  14. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +6 -1
  15. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +143 -52
  16. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +2 -1
  17. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +39 -20
  18. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +2 -1
  19. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -1
  20. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +70 -11
  21. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb +27 -20
  22. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +334 -0
  23. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +28 -22
  24. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +24 -28
  25. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +13 -11
  26. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +1 -1
  27. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +72 -69
  28. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +112 -6
  29. data/spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb +49 -1
  30. data/spec/spec_helper.rb +97 -19
  31. metadata +33 -22
  32. data/Manifest.txt +0 -32
  33. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +0 -126
@@ -6,7 +6,8 @@ module ActiveRecord
6
6
  class OracleEnhancedSynonymDefinition < Struct.new(:name, :table_owner, :table_name, :db_link) #:nodoc:
7
7
  end
8
8
 
9
- class OracleEnhancedIndexDefinition < Struct.new(:table, :name, :unique, :tablespace, :columns) #:nodoc:
9
+ class OracleEnhancedIndexDefinition < Struct.new(:table, :name, :unique, :type, :parameters, :statement_parameters,
10
+ :tablespace, :columns) #:nodoc:
10
11
  end
11
12
 
12
13
  module OracleEnhancedSchemaDefinitions #:nodoc:
@@ -11,26 +11,30 @@ module ActiveRecord #:nodoc:
11
11
  end
12
12
 
13
13
  private
14
-
14
+
15
+ def ignore_table?(table)
16
+ [ActiveRecord::Migrator.proper_table_name('schema_migrations'), ignore_tables].flatten.any? do |ignored|
17
+ case ignored
18
+ when String; table == ignored
19
+ when Regexp; table =~ ignored
20
+ else
21
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
22
+ end
23
+ end
24
+ end
25
+
15
26
  def tables_with_oracle_enhanced(stream)
16
- sorted_tables = @connection.tables.sort
27
+ return tables_without_oracle_enhanced(stream) unless @connection.respond_to?(:materialized_views)
28
+ # do not include materialized views in schema dump - they should be created separately after schema creation
29
+ sorted_tables = (@connection.tables - @connection.materialized_views).sort
17
30
  sorted_tables.each do |tbl|
18
31
  # add table prefix or suffix for schema_migrations
19
- next if [ActiveRecord::Migrator.proper_table_name('schema_migrations'), ignore_tables].flatten.any? do |ignored|
20
- case ignored
21
- when String; tbl == ignored
22
- when Regexp; tbl =~ ignored
23
- else
24
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
25
- end
26
- end
32
+ next if ignore_table? tbl
27
33
  # change table name inspect method
28
34
  tbl.extend TableInspect
29
35
  oracle_enhanced_table(tbl, stream)
30
36
  # add primary key trigger if table has it
31
37
  primary_key_trigger(tbl, stream)
32
- end
33
- sorted_tables.each do |tbl|
34
38
  # add foreign keys if table has them
35
39
  foreign_keys(tbl, stream)
36
40
  end
@@ -76,6 +80,7 @@ module ActiveRecord #:nodoc:
76
80
  if @connection.respond_to?(:synonyms)
77
81
  syns = @connection.synonyms
78
82
  syns.each do |syn|
83
+ next if ignore_table? syn.name
79
84
  table_name = syn.table_name
80
85
  table_name = "#{syn.table_owner}.#{table_name}" if syn.table_owner
81
86
  table_name = "#{table_name}@#{syn.db_link}" if syn.db_link
@@ -89,14 +94,28 @@ module ActiveRecord #:nodoc:
89
94
  def indexes_with_oracle_enhanced(table, stream)
90
95
  if (indexes = @connection.indexes(table)).any?
91
96
  add_index_statements = indexes.map do |index|
92
- # use table.inspect as it will remove prefix and suffix
93
- statment_parts = [ ('add_index ' + table.inspect) ]
94
- statment_parts << index.columns.inspect
95
- statment_parts << (':name => ' + index.name.inspect)
96
- statment_parts << ':unique => true' if index.unique
97
- statment_parts << ':tablespace => ' + index.tablespace.inspect if index.tablespace
98
-
99
- ' ' + statment_parts.join(', ')
97
+ case index.type
98
+ when nil
99
+ # use table.inspect as it will remove prefix and suffix
100
+ statement_parts = [ ('add_index ' + table.inspect) ]
101
+ statement_parts << index.columns.inspect
102
+ statement_parts << (':name => ' + index.name.inspect)
103
+ statement_parts << ':unique => true' if index.unique
104
+ statement_parts << ':tablespace => ' + index.tablespace.inspect if index.tablespace
105
+ when 'CTXSYS.CONTEXT'
106
+ if index.statement_parameters
107
+ statement_parts = [ ('add_context_index ' + table.inspect) ]
108
+ statement_parts << index.statement_parameters
109
+ else
110
+ statement_parts = [ ('add_context_index ' + table.inspect) ]
111
+ statement_parts << index.columns.inspect
112
+ statement_parts << (':name => ' + index.name.inspect)
113
+ end
114
+ else
115
+ # unrecognized index type
116
+ statement_parts = ["# unrecognized index #{index.name.inspect} with type #{index.type.inspect}"]
117
+ end
118
+ ' ' + statement_parts.join(', ')
100
119
  end
101
120
 
102
121
  stream.puts add_index_statements.sort.join("\n")
@@ -1,5 +1,6 @@
1
1
  # implementation idea taken from JDBC adapter
2
- if defined?(Rake.application) && Rake.application && ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] == 'oracle_enhanced'
2
+ if defined?(Rake.application) && Rake.application &&
3
+ ActiveRecord::Base.configurations[defined?(Rails.env) ? Rails.env : RAILS_ENV]['adapter'] == 'oracle_enhanced'
3
4
  oracle_enhanced_rakefile = File.dirname(__FILE__) + "/oracle_enhanced.rake"
4
5
  if Rake.application.lookup("environment")
5
6
  # rails tasks already defined; load the override tasks now
@@ -1 +1 @@
1
- ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = '1.2.3'
1
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = File.read(File.dirname(__FILE__)+'/../../../VERSION').chomp
@@ -112,7 +112,7 @@ describe "OracleEnhancedAdapter" do
112
112
  before(:all) do
113
113
  @conn.execute <<-SQL
114
114
  CREATE TABLE test_employees (
115
- id NUMBER,
115
+ id NUMBER PRIMARY KEY,
116
116
  first_name VARCHAR2(20),
117
117
  last_name VARCHAR2(25),
118
118
  email VARCHAR2(25),
@@ -180,7 +180,7 @@ describe "OracleEnhancedAdapter" do
180
180
  @conn.execute "DROP TABLE test_employees" rescue nil
181
181
  @conn.execute <<-SQL
182
182
  CREATE TABLE test_employees (
183
- id NUMBER,
183
+ id NUMBER PRIMARY KEY,
184
184
  first_name VARCHAR2(20),
185
185
  last_name VARCHAR2(25),
186
186
  hire_date DATE
@@ -198,12 +198,15 @@ describe "OracleEnhancedAdapter" do
198
198
  end
199
199
 
200
200
  before(:each) do
201
- @buffer = StringIO.new
202
- log_to @buffer
201
+ set_logger
203
202
  @conn = ActiveRecord::Base.connection
204
203
  @conn.clear_columns_cache
205
204
  end
206
205
 
206
+ after(:each) do
207
+ clear_logger
208
+ end
209
+
207
210
  describe "without column caching" do
208
211
 
209
212
  before(:each) do
@@ -212,14 +215,26 @@ describe "OracleEnhancedAdapter" do
212
215
 
213
216
  it "should get columns from database at first time" do
214
217
  TestEmployee.connection.columns('test_employees').map(&:name).should == @column_names
215
- @buffer.string.should =~ /select .* from all_tab_columns/im
218
+ @logger.logged(:debug).last.should =~ /select .* from all_tab_columns/im
216
219
  end
217
220
 
218
221
  it "should get columns from database at second time" do
219
222
  TestEmployee.connection.columns('test_employees')
220
- @buffer.truncate(0)
223
+ @logger.clear(:debug)
221
224
  TestEmployee.connection.columns('test_employees').map(&:name).should == @column_names
222
- @buffer.string.should =~ /select .* from all_tab_columns/im
225
+ @logger.logged(:debug).last.should =~ /select .* from all_tab_columns/im
226
+ end
227
+
228
+ it "should get primary key from database at first time" do
229
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
230
+ @logger.logged(:debug).last.should =~ /select .* from all_constraints/im
231
+ end
232
+
233
+ it "should get primary key from database at first time" do
234
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
235
+ @logger.clear(:debug)
236
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
237
+ @logger.logged(:debug).last.should =~ /select .* from all_constraints/im
223
238
  end
224
239
 
225
240
  end
@@ -232,14 +247,26 @@ describe "OracleEnhancedAdapter" do
232
247
 
233
248
  it "should get columns from database at first time" do
234
249
  TestEmployee.connection.columns('test_employees').map(&:name).should == @column_names
235
- @buffer.string.should =~ /select .* from all_tab_columns/im
250
+ @logger.logged(:debug).last.should =~ /select .* from all_tab_columns/im
236
251
  end
237
252
 
238
253
  it "should get columns from cache at second time" do
239
254
  TestEmployee.connection.columns('test_employees')
240
- @buffer.truncate(0)
255
+ @logger.clear(:debug)
241
256
  TestEmployee.connection.columns('test_employees').map(&:name).should == @column_names
242
- @buffer.string.should be_blank
257
+ @logger.logged(:debug).last.should be_blank
258
+ end
259
+
260
+ it "should get primary key from database at first time" do
261
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
262
+ @logger.logged(:debug).last.should =~ /select .* from all_constraints/im
263
+ end
264
+
265
+ it "should get primary key from cache at first time" do
266
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
267
+ @logger.clear(:debug)
268
+ TestEmployee.connection.pk_and_sequence_for('test_employees').should == ['id', nil]
269
+ @logger.logged(:debug).last.should be_blank
243
270
  end
244
271
 
245
272
  end
@@ -329,6 +356,14 @@ describe "OracleEnhancedAdapter" do
329
356
  @adapter.valid_table_name?("sys.v$session").should be_true
330
357
  end
331
358
 
359
+ it "should be valid with upcase schema name" do
360
+ @adapter.valid_table_name?("ABC_123.DEF_456").should be_true
361
+ end
362
+
363
+ it "should be valid with irregular schema name and database links" do
364
+ @adapter.valid_table_name?('abc$#_123.abc$#_123@abc$#@._123').should be_true
365
+ end
366
+
332
367
  it "should not be valid with two dots in name" do
333
368
  @adapter.valid_table_name?("abc_123.def_456.ghi_789").should be_false
334
369
  end
@@ -337,6 +372,30 @@ describe "OracleEnhancedAdapter" do
337
372
  @adapter.valid_table_name?("warehouse-things").should be_false
338
373
  end
339
374
 
375
+ it "should not be valid with for camel-case" do
376
+ @adapter.valid_table_name?("Abc").should be_false
377
+ @adapter.valid_table_name?("aBc").should be_false
378
+ @adapter.valid_table_name?("abC").should be_false
379
+ end
380
+
381
+ it "should not be valid for names > 30 characters" do
382
+ @adapter.valid_table_name?("a" * 31).should be_false
383
+ end
384
+
385
+ it "should not be valid for schema names > 30 characters" do
386
+ @adapter.valid_table_name?(("a" * 31) + ".validname").should be_false
387
+ end
388
+
389
+ it "should not be valid for database links > 128 characters" do
390
+ @adapter.valid_table_name?("name@" + "a" * 129).should be_false
391
+ end
392
+
393
+ it "should not be valid for names that do not begin with alphabetic characters" do
394
+ @adapter.valid_table_name?("1abc").should be_false
395
+ @adapter.valid_table_name?("_abc").should be_false
396
+ @adapter.valid_table_name?("abc.1xyz").should be_false
397
+ @adapter.valid_table_name?("abc._xyz").should be_false
398
+ end
340
399
  end
341
400
 
342
401
  describe "table quoting" do
@@ -415,7 +474,7 @@ describe "OracleEnhancedAdapter" do
415
474
  @db_link_password = SYSTEM_CONNECTION_PARAMS[:password]
416
475
  @db_link_database = SYSTEM_CONNECTION_PARAMS[:database]
417
476
  @conn.execute "DROP DATABASE LINK #{@db_link}" rescue nil
418
- @conn.execute "CREATE DATABASE LINK #{@db_link} CONNECT TO #{@db_link_username} IDENTIFIED BY #{@db_link_password} USING '#{@db_link_database}'"
477
+ @conn.execute "CREATE DATABASE LINK #{@db_link} CONNECT TO #{@db_link_username} IDENTIFIED BY \"#{@db_link_password}\" USING '#{@db_link_database}'"
419
478
  @conn.execute "CREATE OR REPLACE SYNONYM test_posts FOR test_posts@#{@db_link}"
420
479
  @conn.execute "CREATE OR REPLACE SYNONYM test_posts_seq FOR test_posts_seq@#{@db_link}"
421
480
  class ::TestPost < ActiveRecord::Base
@@ -58,7 +58,7 @@ describe "OracleEnhancedAdapter structure dump" do
58
58
  SQL
59
59
  dump = ActiveRecord::Base.connection.structure_dump_fk_constraints
60
60
  dump.split('\n').length.should == 1
61
- dump.should =~ /ALTER TABLE TEST_POSTS ADD CONSTRAINT fk_test_post_foo FOREIGN KEY \(foo_id\) REFERENCES foos\(id\)/
61
+ dump.should =~ /ALTER TABLE \"?TEST_POSTS\"? ADD CONSTRAINT \"?FK_TEST_POST_FOO\"? FOREIGN KEY \(\"?FOO_ID\"?\) REFERENCES \"?FOOS\"?\(\"?ID\"?\)/i
62
62
  end
63
63
 
64
64
  it "should not error when no foreign keys are present" do
@@ -78,7 +78,7 @@ describe "OracleEnhancedAdapter structure dump" do
78
78
  END;
79
79
  SQL
80
80
  dump = ActiveRecord::Base.connection.structure_dump_db_stored_code.gsub(/\n|\s+/,' ')
81
- dump.should =~ /create or replace TRIGGER TEST_POST_TRIGGER/
81
+ dump.should =~ /CREATE OR REPLACE TRIGGER TEST_POST_TRIGGER/
82
82
  end
83
83
 
84
84
  it "should dump types" do
@@ -86,7 +86,7 @@ describe "OracleEnhancedAdapter structure dump" do
86
86
  create or replace TYPE TEST_TYPE AS TABLE OF VARCHAR2(10);
87
87
  SQL
88
88
  dump = ActiveRecord::Base.connection.structure_dump_db_stored_code.gsub(/\n|\s+/,' ')
89
- dump.should =~ /create or replace TYPE TEST_TYPE/
89
+ dump.should =~ /CREATE OR REPLACE TYPE TEST_TYPE/
90
90
  end
91
91
 
92
92
  it "should dump virtual columns" do
@@ -99,7 +99,7 @@ describe "OracleEnhancedAdapter structure dump" do
99
99
  )
100
100
  SQL
101
101
  dump = ActiveRecord::Base.connection.structure_dump
102
- dump.should =~ /id_plus number GENERATED ALWAYS AS \(ID\+2\) VIRTUAL/
102
+ dump.should =~ /ID_PLUS NUMBER GENERATED ALWAYS AS \(ID\+2\) VIRTUAL/
103
103
  end
104
104
 
105
105
  it "should dump unique keys" do
@@ -124,9 +124,9 @@ describe "OracleEnhancedAdapter structure dump" do
124
124
  SQL
125
125
 
126
126
  dump = ActiveRecord::Base.connection.structure_dump
127
- dump.should =~ /create unique index ix_test_posts_foo_id on test_posts \(foo_id\)/i
128
- dump.should =~ /create index ix_test_posts_foo on test_posts \(foo\)/i
129
- dump.should_not =~ /create unique index uk_test_posts_/i
127
+ dump.should =~ /CREATE UNIQUE INDEX "?IX_TEST_POSTS_FOO_ID"? ON "?TEST_POSTS"? \("?FOO_ID"?\)/i
128
+ dump.should =~ /CREATE INDEX "?IX_TEST_POSTS_FOO\"? ON "?TEST_POSTS"? \("?FOO"?\)/i
129
+ dump.should_not =~ /CREATE UNIQUE INDEX "?UK_TEST_POSTS_/i
130
130
  end
131
131
  end
132
132
  describe "temporary tables" do
@@ -138,7 +138,7 @@ describe "OracleEnhancedAdapter structure dump" do
138
138
  t.integer :post_id
139
139
  end
140
140
  dump = ActiveRecord::Base.connection.structure_dump
141
- dump.should =~ /create global temporary table test_comments/i
141
+ dump.should =~ /CREATE GLOBAL TEMPORARY TABLE "?TEST_COMMENTS"?/i
142
142
  end
143
143
  end
144
144
 
@@ -173,8 +173,8 @@ describe "OracleEnhancedAdapter structure dump" do
173
173
  end
174
174
  it "should dump drop sql for just temp tables" do
175
175
  dump = @conn.temp_table_drop
176
- dump.should =~ /drop table temp_tbl/i
177
- dump.should_not =~ /drop table not_temp_tbl/i
176
+ dump.should =~ /DROP TABLE "TEMP_TBL"/
177
+ dump.should_not =~ /DROP TABLE "?NOT_TEMP_TBL"?/i
178
178
  end
179
179
  after(:each) do
180
180
  @conn.drop_table :temp_tbl
@@ -194,6 +194,10 @@ describe "OracleEnhancedAdapter structure dump" do
194
194
  @conn.execute <<-SQL
195
195
  create or replace view full_drop_test_view (foo) as select id as "foo" from full_drop_test
196
196
  SQL
197
+ #materialized view
198
+ @conn.execute <<-SQL
199
+ create materialized view full_drop_test_mview (foo) as select id as "foo" from full_drop_test
200
+ SQL
197
201
  #package
198
202
  @conn.execute <<-SQL
199
203
  create or replace package full_drop_test_package as
@@ -241,6 +245,7 @@ describe "OracleEnhancedAdapter structure dump" do
241
245
  @conn.drop_table :full_drop_test
242
246
  @conn.drop_table :full_drop_test_temp
243
247
  @conn.execute "DROP VIEW FULL_DROP_TEST_VIEW" rescue nil
248
+ @conn.execute "DROP MATERIALIZED VIEW FULL_DROP_TEST_MVIEW" rescue nil
244
249
  @conn.execute "DROP SYNONYM FULL_DROP_TEST_SYNONYM" rescue nil
245
250
  @conn.execute "DROP PACKAGE FULL_DROP_TEST_PACKAGE" rescue nil
246
251
  @conn.execute "DROP FUNCTION FULL_DROP_TEST_FUNCTION" rescue nil
@@ -249,19 +254,21 @@ describe "OracleEnhancedAdapter structure dump" do
249
254
  end
250
255
  it "should contain correct sql" do
251
256
  drop = @conn.full_drop
252
- drop.should =~ /drop table full_drop_test cascade constraints/i
253
- drop.should =~ /drop sequence full_drop_test_seq/i
254
- drop.should =~ /drop view "full_drop_test_view"/i
255
- drop.should =~ /drop package full_drop_test_package/i
256
- drop.should =~ /drop function full_drop_test_function/i
257
- drop.should =~ /drop procedure full_drop_test_procedure/i
258
- drop.should =~ /drop synonym "full_drop_test_synonym"/i
259
- drop.should =~ /drop type "full_drop_test_type"/i
257
+ drop.should =~ /DROP TABLE "FULL_DROP_TEST" CASCADE CONSTRAINTS/
258
+ drop.should =~ /DROP SEQUENCE "FULL_DROP_TEST_SEQ"/
259
+ drop.should =~ /DROP VIEW "FULL_DROP_TEST_VIEW"/
260
+ drop.should_not =~ /DROP TABLE "?FULL_DROP_TEST_MVIEW"?/i
261
+ drop.should =~ /DROP MATERIALIZED VIEW "FULL_DROP_TEST_MVIEW"/
262
+ drop.should =~ /DROP PACKAGE "FULL_DROP_TEST_PACKAGE"/
263
+ drop.should =~ /DROP FUNCTION "FULL_DROP_TEST_FUNCTION"/
264
+ drop.should =~ /DROP PROCEDURE "FULL_DROP_TEST_PROCEDURE"/
265
+ drop.should =~ /DROP SYNONYM "FULL_DROP_TEST_SYNONYM"/
266
+ drop.should =~ /DROP TYPE "FULL_DROP_TEST_TYPE"/
260
267
  end
261
268
  it "should not drop tables when preserve_tables is true" do
262
269
  drop = @conn.full_drop(true)
263
- drop.should =~ /drop table full_drop_test_temp/i
264
- drop.should_not =~ /drop table full_drop_test cascade constraints/i
270
+ drop.should =~ /DROP TABLE "FULL_DROP_TEST_TEMP"/
271
+ drop.should_not =~ /DROP TABLE "?FULL_DROP_TEST"? CASCADE CONSTRAINTS/i
265
272
  end
266
273
  end
267
274
  end
@@ -0,0 +1,334 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe "OracleEnhancedAdapter context index" do
4
+ include SchemaSpecHelper
5
+ include LoggerSpecHelper
6
+
7
+ def create_table_posts
8
+ schema_define do
9
+ create_table :posts, :force => true do |t|
10
+ t.string :title
11
+ t.text :body
12
+ t.integer :comments_count
13
+ t.timestamps
14
+ t.string :all_text, :limit => 2 # will be used for multi-column index
15
+ end
16
+ end
17
+ end
18
+
19
+ def create_table_comments
20
+ schema_define do
21
+ create_table :comments, :force => true do |t|
22
+ t.integer :post_id
23
+ t.string :author
24
+ t.text :body
25
+ t.timestamps
26
+ end
27
+ end
28
+ end
29
+
30
+ def create_tables
31
+ create_table_posts
32
+ create_table_comments
33
+ end
34
+
35
+ def drop_table_posts
36
+ schema_define { drop_table :posts }
37
+ end
38
+
39
+ def drop_table_comments
40
+ schema_define { drop_table :comments }
41
+ end
42
+
43
+ def drop_tables
44
+ drop_table_comments
45
+ drop_table_posts
46
+ end
47
+
48
+ before(:all) do
49
+ # database user should have CTXAPP role to be able to set CONTEXT index parameters
50
+ ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
51
+ @conn = ActiveRecord::Base.connection
52
+ end
53
+
54
+ describe "on single table" do
55
+ before(:all) do
56
+ @title_words = %w{aaa bbb ccc}
57
+ @body_words = %w{foo bar baz}
58
+ create_table_posts
59
+ class ::Post < ActiveRecord::Base
60
+ has_context_index
61
+ end
62
+ @post0 = Post.create(:title => "dummy title", :body => "dummy body")
63
+ @post1 = Post.create(:title => @title_words.join(' '), :body => @body_words.join(' '))
64
+ @post2 = Post.create(:title => (@title_words*2).join(' '), :body => (@body_words*2).join(' '))
65
+ end
66
+
67
+ after(:all) do
68
+ drop_table_posts
69
+ Object.send(:remove_const, "Post")
70
+ end
71
+
72
+ after(:each) do
73
+ @post.destroy if @post
74
+ end
75
+
76
+ it "should create single VARCHAR2 column index" do
77
+ @conn.add_context_index :posts, :title
78
+ @title_words.each do |word|
79
+ Post.contains(:title, word).all.should == [@post2, @post1]
80
+ end
81
+ @conn.remove_context_index :posts, :title
82
+ end
83
+
84
+ it "should create single CLOB column index" do
85
+ @conn.add_context_index :posts, :body
86
+ @body_words.each do |word|
87
+ Post.contains(:body, word).all.should == [@post2, @post1]
88
+ end
89
+ @conn.remove_context_index :posts, :body
90
+ end
91
+
92
+ it "should not include text index secondary tables in user tables list" do
93
+ @conn.add_context_index :posts, :title
94
+ @conn.tables.any?{|t| t =~ /^dr\$/i}.should be_false
95
+ @conn.remove_context_index :posts, :title
96
+ end
97
+
98
+ it "should create multiple column index" do
99
+ @conn.add_context_index :posts, [:title, :body]
100
+ (@title_words+@body_words).each do |word|
101
+ Post.contains(:title, word).all.should == [@post2, @post1]
102
+ end
103
+ @conn.remove_context_index :posts, [:title, :body]
104
+ end
105
+
106
+ it "should create multiple column index with specified main index column" do
107
+ @conn.add_context_index :posts, [:title, :body],
108
+ :index_column => :all_text, :sync => 'ON COMMIT'
109
+ @post = Post.create(:title => "abc", :body => "def")
110
+ Post.contains(:all_text, "abc").all.should == [@post]
111
+ Post.contains(:all_text, "def").all.should == [@post]
112
+ @post.update_attributes!(:title => "ghi")
113
+ # index will not be updated as all_text column is not changed
114
+ Post.contains(:all_text, "ghi").all.should be_empty
115
+ @post.update_attributes!(:all_text => "1")
116
+ # index will be updated when all_text column is changed
117
+ Post.contains(:all_text, "ghi").all.should == [@post]
118
+ @conn.remove_context_index :posts, :index_column => :all_text
119
+ end
120
+
121
+ it "should create multiple column index with trigger updated main index column" do
122
+ @conn.add_context_index :posts, [:title, :body],
123
+ :index_column => :all_text, :index_column_trigger_on => [:created_at, :updated_at],
124
+ :sync => 'ON COMMIT'
125
+ @post = Post.create(:title => "abc", :body => "def")
126
+ Post.contains(:all_text, "abc").all.should == [@post]
127
+ Post.contains(:all_text, "def").all.should == [@post]
128
+ @post.update_attributes!(:title => "ghi")
129
+ # index should be updated as created_at column is changed
130
+ Post.contains(:all_text, "ghi").all.should == [@post]
131
+ @conn.remove_context_index :posts, :index_column => :all_text
132
+ end
133
+
134
+ end
135
+
136
+ describe "on multiple tables" do
137
+ before(:all) do
138
+ create_tables
139
+ class ::Post < ActiveRecord::Base
140
+ has_many :comments, :dependent => :destroy
141
+ has_context_index
142
+ end
143
+ class ::Comment < ActiveRecord::Base
144
+ belongs_to :post, :counter_cache => true
145
+ end
146
+ end
147
+
148
+ after(:all) do
149
+ drop_tables
150
+ Object.send(:remove_const, "Comment")
151
+ Object.send(:remove_const, "Post")
152
+ end
153
+
154
+ after(:each) do
155
+ Post.destroy_all
156
+ end
157
+
158
+ it "should create multiple table index with specified main index column" do
159
+ @conn.add_context_index :posts,
160
+ [:title, :body,
161
+ # specify aliases always with AS keyword
162
+ "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
163
+ ],
164
+ :name => 'post_and_comments_index',
165
+ :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
166
+ :sync => 'ON COMMIT'
167
+ @post = Post.create!(:title => "aaa", :body => "bbb")
168
+ @post.comments.create!(:author => "ccc", :body => "ddd")
169
+ @post.comments.create!(:author => "eee", :body => "fff")
170
+ ["aaa", "bbb", "ccc", "ddd", "eee", "fff"].each do |word|
171
+ Post.contains(:all_text, word).all.should == [@post]
172
+ end
173
+ @conn.remove_context_index :posts, :name => 'post_and_comments_index'
174
+ end
175
+
176
+ it "should find by search term within specified field" do
177
+ @post = Post.create!(:title => "aaa", :body => "bbb")
178
+ @post.comments.create!(:author => "ccc", :body => "ddd")
179
+ @conn.add_context_index :posts,
180
+ [:title, :body,
181
+ # specify aliases always with AS keyword
182
+ "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
183
+ ],
184
+ :index_column => :all_text
185
+ Post.contains(:all_text, "aaa within title").all.should == [@post]
186
+ Post.contains(:all_text, "aaa within body").all.should be_empty
187
+ Post.contains(:all_text, "bbb within body").all.should == [@post]
188
+ Post.contains(:all_text, "bbb within title").all.should be_empty
189
+ Post.contains(:all_text, "ccc within comment_author").all.should == [@post]
190
+ Post.contains(:all_text, "ccc within comment_body").all.should be_empty
191
+ Post.contains(:all_text, "ddd within comment_body").all.should == [@post]
192
+ Post.contains(:all_text, "ddd within comment_author").all.should be_empty
193
+ @conn.remove_context_index :posts, :index_column => :all_text
194
+ end
195
+
196
+ end
197
+
198
+ describe "with specified tablespace" do
199
+ before(:all) do
200
+ create_table_posts
201
+ class ::Post < ActiveRecord::Base
202
+ has_context_index
203
+ end
204
+ @post = Post.create(:title => 'aaa', :body => 'bbb')
205
+ @tablespace = @conn.default_tablespace
206
+ set_logger
207
+ @conn = ActiveRecord::Base.connection
208
+ end
209
+
210
+ after(:all) do
211
+ drop_table_posts
212
+ Object.send(:remove_const, "Post")
213
+ end
214
+
215
+ after(:each) do
216
+ clear_logger
217
+ end
218
+
219
+ def verify_logged_statements
220
+ ['K_TABLE_CLAUSE', 'R_TABLE_CLAUSE', 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
221
+ @logger.output(:debug).should =~ /CTX_DDL\.SET_ATTRIBUTE\('index_posts_on_title_sto', '#{clause}', '.*TABLESPACE #{@tablespace}'\)/
222
+ end
223
+ @logger.output(:debug).should =~ /CREATE INDEX .* PARAMETERS \('STORAGE index_posts_on_title_sto'\)/
224
+ end
225
+
226
+ it "should create index on single column" do
227
+ @conn.add_context_index :posts, :title, :tablespace => @tablespace
228
+ verify_logged_statements
229
+ Post.contains(:title, 'aaa').all.should == [@post]
230
+ @conn.remove_context_index :posts, :title
231
+ end
232
+
233
+ it "should create index on multiple columns" do
234
+ @conn.add_context_index :posts, [:title, :body], :name => 'index_posts_text', :tablespace => @conn.default_tablespace
235
+ verify_logged_statements
236
+ Post.contains(:title, 'aaa AND bbb').all.should == [@post]
237
+ @conn.remove_context_index :posts, :name => 'index_posts_text'
238
+ end
239
+
240
+ end
241
+
242
+ describe "schema dump" do
243
+
244
+ def standard_dump
245
+ stream = StringIO.new
246
+ ActiveRecord::SchemaDumper.ignore_tables = []
247
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
248
+ stream.string
249
+ end
250
+
251
+ describe "without table prefixe and suffix" do
252
+
253
+ before(:all) do
254
+ create_tables
255
+ end
256
+
257
+ after(:all) do
258
+ drop_tables
259
+ end
260
+
261
+ it "should dump definition of single column index" do
262
+ @conn.add_context_index :posts, :title
263
+ standard_dump.should =~ /add_context_index "posts", \["title"\], :name => \"index_posts_on_title\"$/
264
+ @conn.remove_context_index :posts, :title
265
+ end
266
+
267
+ it "should dump definition of multiple column index" do
268
+ @conn.add_context_index :posts, [:title, :body]
269
+ standard_dump.should =~ /add_context_index "posts", \[:title, :body\]$/
270
+ @conn.remove_context_index :posts, [:title, :body]
271
+ end
272
+
273
+ it "should dump definition of multiple table index with options" do
274
+ options = {
275
+ :name => 'post_and_comments_index',
276
+ :index_column => :all_text, :index_column_trigger_on => :updated_at,
277
+ :sync => 'ON COMMIT'
278
+ }
279
+ @conn.add_context_index :posts,
280
+ [:title, :body,
281
+ "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
282
+ ], options
283
+ standard_dump.should =~ /add_context_index "posts", \[:title, :body, "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"\], #{options.inspect[1..-2]}$/
284
+ @conn.remove_context_index :posts, :name => 'post_and_comments_index'
285
+ end
286
+
287
+ end
288
+
289
+ describe "with table prefix and suffix" do
290
+ before(:all) do
291
+ ActiveRecord::Base.table_name_prefix = 'xxx_'
292
+ ActiveRecord::Base.table_name_suffix = '_xxx'
293
+ create_tables
294
+ end
295
+
296
+ after(:all) do
297
+ drop_tables
298
+ ActiveRecord::Base.table_name_prefix = ''
299
+ ActiveRecord::Base.table_name_suffix = ''
300
+ end
301
+
302
+ it "should dump definition of single column index" do
303
+ schema_define { add_context_index :posts, :title }
304
+ standard_dump.should =~ /add_context_index "posts", \["title"\], :name => "i_xxx_posts_xxx_title"$/
305
+ schema_define { remove_context_index :posts, :title }
306
+ end
307
+
308
+ it "should dump definition of multiple column index" do
309
+ schema_define { add_context_index :posts, [:title, :body] }
310
+ standard_dump.should =~ /add_context_index "posts", \[:title, :body\]$/
311
+ schema_define { remove_context_index :posts, [:title, :body] }
312
+ end
313
+
314
+ it "should dump definition of multiple table index with options" do
315
+ options = {
316
+ :name => 'xxx_post_and_comments_i',
317
+ :index_column => :all_text, :index_column_trigger_on => :updated_at,
318
+ :sync => 'ON COMMIT'
319
+ }
320
+ schema_define do
321
+ add_context_index :posts,
322
+ [:title, :body,
323
+ "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
324
+ ], options
325
+ end
326
+ standard_dump.should =~ /add_context_index "posts", \[:title, :body, "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"\], #{options.inspect[1..-2]}$/
327
+ schema_define { remove_context_index :posts, :name => 'xxx_post_and_comments_i' }
328
+ end
329
+
330
+ end
331
+
332
+ end
333
+
334
+ end