activerecord-oracle_enhanced-adapter 1.5.6 → 1.6.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/History.md +87 -0
  4. data/README.md +271 -174
  5. data/VERSION +1 -1
  6. data/activerecord-oracle_enhanced-adapter.gemspec +26 -22
  7. data/lib/active_record/connection_adapters/{oracle_enhanced_column.rb → oracle_enhanced/column.rb} +14 -63
  8. data/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb +65 -0
  9. data/lib/active_record/connection_adapters/{oracle_enhanced_connection.rb → oracle_enhanced/connection.rb} +2 -2
  10. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +347 -0
  11. data/lib/active_record/connection_adapters/{oracle_enhanced_cpk.rb → oracle_enhanced/cpk.rb} +0 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +257 -0
  13. data/lib/active_record/connection_adapters/{oracle_enhanced_database_tasks.rb → oracle_enhanced/database_tasks.rb} +0 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb +40 -0
  15. data/lib/active_record/connection_adapters/{oracle_enhanced_jdbc_connection.rb → oracle_enhanced/jdbc_connection.rb} +0 -0
  16. data/lib/active_record/connection_adapters/{oracle_enhanced_oci_connection.rb → oracle_enhanced/oci_connection.rb} +0 -0
  17. data/lib/active_record/connection_adapters/{oracle_enhanced_procedures.rb → oracle_enhanced/procedures.rb} +0 -0
  18. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_creation.rb → oracle_enhanced/schema_creation.rb} +17 -16
  19. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +92 -0
  20. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_dumper.rb → oracle_enhanced/schema_dumper.rb} +4 -32
  21. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +543 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +65 -0
  23. data/lib/active_record/connection_adapters/{oracle_enhanced_structure_dump.rb → oracle_enhanced/structure_dump.rb} +26 -4
  24. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +1 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +159 -66
  26. data/lib/active_record/oracle_enhanced/type/integer.rb +13 -0
  27. data/lib/active_record/oracle_enhanced/type/raw.rb +13 -0
  28. data/lib/active_record/oracle_enhanced/type/timestamp.rb +11 -0
  29. data/lib/activerecord-oracle_enhanced-adapter.rb +1 -1
  30. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +6 -31
  31. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +1 -1
  32. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +2 -2
  33. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +2 -2
  34. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +63 -63
  35. data/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +1 -1
  36. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +7 -13
  37. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +21 -175
  38. data/spec/spec_config.yaml.template +10 -0
  39. data/spec/spec_helper.rb +21 -10
  40. metadata +29 -25
  41. data/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb +0 -77
  42. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +0 -350
  43. data/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb +0 -262
  44. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +0 -45
  45. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +0 -197
  46. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +0 -450
  47. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +0 -258
  48. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +0 -1
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.6
1
+ 1.6.0.beta1
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{activerecord-oracle_enhanced-adapter}
8
- s.version = "1.5.6"
8
+ s.version = "1.6.0.beta1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.license = 'MIT'
12
12
  s.authors = [%q{Raimonds Simanovskis}]
13
- s.date = %q{2015-03-30}
13
+ s.date = %q{2015-06-19}
14
14
  s.description = %q{Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases.
15
15
  This adapter is superset of original ActiveRecord Oracle adapter.
16
16
  }
@@ -30,23 +30,27 @@ This adapter is superset of original ActiveRecord Oracle adapter.
30
30
  "activerecord-oracle_enhanced-adapter.gemspec",
31
31
  "lib/active_record/connection_adapters/emulation/oracle_adapter.rb",
32
32
  "lib/active_record/connection_adapters/oracle_enhanced_adapter.rb",
33
- "lib/active_record/connection_adapters/oracle_enhanced_column.rb",
34
- "lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb",
35
- "lib/active_record/connection_adapters/oracle_enhanced_connection.rb",
36
- "lib/active_record/connection_adapters/oracle_enhanced_context_index.rb",
37
- "lib/active_record/connection_adapters/oracle_enhanced_cpk.rb",
38
- "lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb",
39
- "lib/active_record/connection_adapters/oracle_enhanced_dirty.rb",
40
- "lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb",
41
- "lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb",
42
- "lib/active_record/connection_adapters/oracle_enhanced_procedures.rb",
43
- "lib/active_record/connection_adapters/oracle_enhanced_schema_creation.rb",
44
- "lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb",
45
- "lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb",
46
- "lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb",
47
- "lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb",
48
- "lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb",
49
- "lib/active_record/connection_adapters/oracle_enhanced_version.rb",
33
+ "lib/active_record/connection_adapters/oracle_enhanced/column.rb",
34
+ "lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb",
35
+ "lib/active_record/connection_adapters/oracle_enhanced/connection.rb",
36
+ "lib/active_record/connection_adapters/oracle_enhanced/context_index.rb",
37
+ "lib/active_record/connection_adapters/oracle_enhanced/cpk.rb",
38
+ "lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb",
39
+ "lib/active_record/connection_adapters/oracle_enhanced/dirty.rb",
40
+ "lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb",
41
+ "lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb",
42
+ "lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb",
43
+ "lib/active_record/connection_adapters/oracle_enhanced/procedures.rb",
44
+ "lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb",
45
+ "lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb",
46
+ "lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb",
47
+ "lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb",
48
+ "lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb",
49
+ "lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb",
50
+ "lib/active_record/connection_adapters/oracle_enhanced/version.rb",
51
+ "lib/active_record/oracle_enhanced/type/integer.rb",
52
+ "lib/active_record/oracle_enhanced/type/timestamp.rb",
53
+ "lib/active_record/oracle_enhanced/type/raw.rb",
50
54
  "lib/activerecord-oracle_enhanced-adapter.rb",
51
55
  "spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb",
52
56
  "spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb",
@@ -88,7 +92,7 @@ This adapter is superset of original ActiveRecord Oracle adapter.
88
92
  s.specification_version = 3
89
93
 
90
94
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
91
- s.add_development_dependency(%q<jeweler>, ["~> 1.8"])
95
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0"])
92
96
  s.add_development_dependency(%q<rspec>, ["~> 2.4"])
93
97
  s.add_development_dependency(%q<activerecord>, [">= 0"])
94
98
  s.add_development_dependency(%q<activemodel>, [">= 0"])
@@ -100,7 +104,7 @@ This adapter is superset of original ActiveRecord Oracle adapter.
100
104
  s.add_development_dependency(%q<ruby-plsql>, [">= 0.4.4"])
101
105
  s.add_development_dependency(%q<ruby-oci8>, [">= 2.0.4"])
102
106
  else
103
- s.add_dependency(%q<jeweler>, ["~> 1.8"])
107
+ s.add_dependency(%q<jeweler>, ["~> 2.0"])
104
108
  s.add_dependency(%q<rspec>, ["~> 2.4"])
105
109
  s.add_dependency(%q<activerecord>, [">= 0"])
106
110
  s.add_dependency(%q<activemodel>, [">= 0"])
@@ -113,7 +117,7 @@ This adapter is superset of original ActiveRecord Oracle adapter.
113
117
  s.add_dependency(%q<ruby-oci8>, [">= 2.0.4"])
114
118
  end
115
119
  else
116
- s.add_dependency(%q<jeweler>, ["~> 1.8"])
120
+ s.add_dependency(%q<jeweler>, ["~> 2.0"])
117
121
  s.add_dependency(%q<rspec>, ["~> 2.4"])
118
122
  s.add_dependency(%q<activerecord>, [">= 0"])
119
123
  s.add_dependency(%q<activemodel>, [">= 0"])
@@ -2,11 +2,13 @@ module ActiveRecord
2
2
  module ConnectionAdapters #:nodoc:
3
3
  class OracleEnhancedColumn < Column
4
4
 
5
- attr_reader :table_name, :forced_column_type, :nchar, :virtual_column_data_default, :returning_id #:nodoc:
5
+ attr_reader :table_name, :nchar, :virtual_column_data_default, :returning_id #:nodoc:
6
6
 
7
- def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil, virtual=false, returning_id=false) #:nodoc:
7
+ FALSE_VALUES << 'N'
8
+ TRUE_VALUES << 'Y'
9
+
10
+ def initialize(name, default, cast_type, sql_type = nil, null = true, table_name = nil, virtual=false, returning_id=false) #:nodoc:
8
11
  @table_name = table_name
9
- @forced_column_type = forced_column_type
10
12
  @virtual = virtual
11
13
  @virtual_column_data_default = default.inspect if virtual
12
14
  @returning_id = returning_id
@@ -15,32 +17,20 @@ module ActiveRecord
15
17
  else
16
18
  default_value = self.class.extract_value_from_default(default)
17
19
  end
18
- super(name, default_value, sql_type, null)
20
+ super(name, default_value, cast_type, sql_type, null)
19
21
  # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)?
20
22
  # Define only when needed as adapter "quote" method will check at first if instance variable is defined.
21
- @nchar = true if @type == :string && sql_type[0,1] == 'N'
22
- @object_type = sql_type.include? '.'
23
- end
24
-
25
- def type_cast(value) #:nodoc:
26
- case type
27
- when :raw
28
- OracleEnhancedColumn.string_to_raw(value)
29
- when :datetime
30
- OracleEnhancedAdapter.emulate_dates ? guess_date_or_time(value) : super
31
- when :float
32
- !value.nil? ? self.class.value_to_decimal(value) : super
33
- else
34
- super
23
+ if sql_type
24
+ @nchar = true if cast_type.class == ActiveRecord::Type::String && sql_type[0,1] == 'N'
25
+ @object_type = sql_type.include? '.'
35
26
  end
27
+ # TODO: Need to investigate when `sql_type` becomes nil
36
28
  end
37
29
 
38
- def type_cast_code(var_name)
39
- type == :float ? "#{self.class.name}.value_to_decimal(#{var_name})" : super
40
- end
41
-
42
- def klass
43
- type == :float ? BigDecimal : super
30
+ def type_cast(value) #:nodoc:
31
+ return OracleEnhancedColumn::string_to_raw(value) if type == :raw
32
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
33
+ super
44
34
  end
45
35
 
46
36
  def virtual?
@@ -96,45 +86,6 @@ module ActiveRecord
96
86
 
97
87
  private
98
88
 
99
- def simplified_type(field_type)
100
- forced_column_type ||
101
- case field_type
102
- when /decimal|numeric|number/i
103
- if OracleEnhancedAdapter.emulate_booleans && field_type.upcase == "NUMBER(1)"
104
- :boolean
105
- elsif extract_scale(field_type) == 0 ||
106
- # if column name is ID or ends with _ID
107
- OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
108
- :integer
109
- elsif field_type.upcase == "NUMBER"
110
- OracleEnhancedAdapter.number_datatype_coercion
111
- else
112
- :decimal
113
- end
114
- when /raw/i
115
- :raw
116
- when /char/i
117
- if OracleEnhancedAdapter.emulate_booleans_from_strings &&
118
- OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
119
- :boolean
120
- else
121
- :string
122
- end
123
- when /date/i
124
- if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)
125
- :date
126
- else
127
- :datetime
128
- end
129
- when /timestamp/i
130
- :timestamp
131
- when /time/i
132
- :datetime
133
- else
134
- super
135
- end
136
- end
137
-
138
89
  def self.extract_value_from_default(default)
139
90
  case default
140
91
  when String
@@ -0,0 +1,65 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ module OracleEnhanced #:nodoc:
4
+ module ColumnDumper #:nodoc:
5
+ def self.included(base) #:nodoc:
6
+ base.class_eval do
7
+ private
8
+ alias_method_chain :column_spec, :oracle_enhanced
9
+ alias_method_chain :prepare_column_options, :oracle_enhanced
10
+ alias_method_chain :migration_keys, :oracle_enhanced
11
+
12
+ def oracle_enhanced_adapter?
13
+ # return original method if not using 'OracleEnhanced'
14
+ if (rails_env = defined?(Rails.env) ? Rails.env : (defined?(RAILS_ENV) ? RAILS_ENV : nil)) &&
15
+ ActiveRecord::Base.configurations[rails_env] &&
16
+ ActiveRecord::Base.configurations[rails_env]['adapter'] != 'oracle_enhanced'
17
+ return false
18
+ else
19
+ return true
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def column_spec_with_oracle_enhanced(column, types)
26
+ # return original method if not using 'OracleEnhanced'
27
+ return column_spec_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter?
28
+
29
+ spec = prepare_column_options(column, types)
30
+ (spec.keys - [:name, :type]).each do |k|
31
+ key_s = (k == :virtual_type ? "type: " : "#{k.to_s}: ")
32
+ spec[k] = key_s + spec[k]
33
+ end
34
+ spec
35
+ end
36
+
37
+ def prepare_column_options_with_oracle_enhanced(column, types)
38
+ # return original method if not using 'OracleEnhanced'
39
+ return prepare_column_options_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter?
40
+
41
+ spec = {}
42
+ spec[:name] = column.name.inspect
43
+ spec[:type] = column.virtual? ? 'virtual' : column.type.to_s
44
+ spec[:virtual_type] = column.type.inspect if column.virtual? && column.sql_type != 'NUMBER'
45
+ spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && column.type != :decimal
46
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
47
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
48
+ spec[:null] = 'false' if !column.null
49
+ spec[:as] = column.virtual_column_data_default if column.virtual?
50
+ spec[:default] = schema_default(column) if column.has_default? && !column.virtual?
51
+ spec.delete(:default) if spec[:default].nil?
52
+ spec
53
+ end
54
+
55
+ def migration_keys_with_oracle_enhanced
56
+ # TODO `& column_specs.map(&:keys).flatten` should be exetuted here
57
+ # return original method if not using 'OracleEnhanced'
58
+ return migration_keys_without_oracle_enhanced unless oracle_enhanced_adapter?
59
+
60
+ [:name, :limit, :precision, :scale, :default, :null, :as, :virtual_type]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -109,11 +109,11 @@ end
109
109
  # if MRI or YARV
110
110
  if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
111
111
  ORACLE_ENHANCED_CONNECTION = :oci
112
- require 'active_record/connection_adapters/oracle_enhanced_oci_connection'
112
+ require 'active_record/connection_adapters/oracle_enhanced/oci_connection'
113
113
  # if JRuby
114
114
  elsif RUBY_ENGINE == 'jruby'
115
115
  ORACLE_ENHANCED_CONNECTION = :jdbc
116
- require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection'
116
+ require 'active_record/connection_adapters/oracle_enhanced/jdbc_connection'
117
117
  else
118
118
  raise "Unsupported Ruby engine #{RUBY_ENGINE}"
119
119
  end
@@ -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_column_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