activerecord-oracle_enhanced-adapter 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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