activerecord-oracle_enhanced-adapter 1.5.6 → 1.6.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/History.md +107 -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/database_statements.rb +257 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb +40 -0
  13. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_creation.rb → oracle_enhanced/schema_creation.rb} +17 -16
  14. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +95 -0
  15. data/lib/active_record/connection_adapters/{oracle_enhanced_schema_dumper.rb → oracle_enhanced/schema_dumper.rb} +4 -32
  16. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +546 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +65 -0
  18. data/lib/active_record/connection_adapters/{oracle_enhanced_structure_dump.rb → oracle_enhanced/structure_dump.rb} +26 -4
  19. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +1 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +159 -66
  21. data/lib/active_record/oracle_enhanced/type/integer.rb +13 -0
  22. data/lib/active_record/oracle_enhanced/type/raw.rb +13 -0
  23. data/lib/active_record/oracle_enhanced/type/timestamp.rb +11 -0
  24. data/lib/activerecord-oracle_enhanced-adapter.rb +1 -1
  25. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +6 -31
  26. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +1 -1
  27. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +2 -2
  28. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +2 -2
  29. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +63 -63
  30. data/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +1 -1
  31. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +7 -13
  32. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +25 -178
  33. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +14 -5
  34. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +1 -0
  35. data/spec/spec_config.yaml.template +10 -0
  36. data/spec/spec_helper.rb +21 -10
  37. metadata +27 -23
  38. data/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb +0 -77
  39. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +0 -350
  40. data/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb +0 -262
  41. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +0 -45
  42. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +0 -197
  43. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +0 -450
  44. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +0 -258
  45. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +0 -1
  46. /data/lib/active_record/connection_adapters/{oracle_enhanced_cpk.rb → oracle_enhanced/cpk.rb} +0 -0
  47. /data/lib/active_record/connection_adapters/{oracle_enhanced_database_tasks.rb → oracle_enhanced/database_tasks.rb} +0 -0
  48. /data/lib/active_record/connection_adapters/{oracle_enhanced_jdbc_connection.rb → oracle_enhanced/jdbc_connection.rb} +0 -0
  49. /data/lib/active_record/connection_adapters/{oracle_enhanced_oci_connection.rb → oracle_enhanced/oci_connection.rb} +0 -0
  50. /data/lib/active_record/connection_adapters/{oracle_enhanced_procedures.rb → oracle_enhanced/procedures.rb} +0 -0
@@ -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"
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-25}
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