ctreatma-activerecord-oracle_enhanced-adapter 1.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +51 -0
  3. data/History.md +269 -0
  4. data/License.txt +20 -0
  5. data/README.md +378 -0
  6. data/RUNNING_TESTS.md +45 -0
  7. data/Rakefile +46 -0
  8. data/VERSION +1 -0
  9. data/activerecord-oracle_enhanced-adapter.gemspec +130 -0
  10. data/ctreatma-activerecord-oracle_enhanced-adapter.gemspec +129 -0
  11. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1390 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +106 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +136 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +119 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +328 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +553 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +492 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +213 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +252 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +373 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +265 -0
  29. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +290 -0
  30. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
  31. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  32. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +749 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +310 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +426 -0
  36. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
  37. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
  38. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1330 -0
  39. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
  40. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +121 -0
  41. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  42. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +374 -0
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +380 -0
  44. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1112 -0
  45. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +323 -0
  46. data/spec/spec_helper.rb +185 -0
  47. metadata +287 -0
@@ -0,0 +1,106 @@
1
+ module ActiveRecord
2
+ class Base
3
+ # Establishes a connection to the database that's used by all Active Record objects.
4
+ def self.oracle_enhanced_connection(config) #:nodoc:
5
+ if config[:emulate_oracle_adapter] == true
6
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
7
+ # conditionals in the rails activerecord test suite
8
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
9
+ ConnectionAdapters::OracleAdapter.new(
10
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger, config)
11
+ else
12
+ ConnectionAdapters::OracleEnhancedAdapter.new(
13
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger, config)
14
+ end
15
+ end
16
+
17
+ # Specify table columns which should be ignored by ActiveRecord, e.g.:
18
+ #
19
+ # ignore_table_columns :attribute1, :attribute2
20
+ def self.ignore_table_columns(*args)
21
+ connection.ignore_table_columns(table_name,*args)
22
+ end
23
+
24
+ # Specify which table columns should be typecasted to Date (without time), e.g.:
25
+ #
26
+ # set_date_columns :created_on, :updated_on
27
+ def self.set_date_columns(*args)
28
+ connection.set_type_for_columns(table_name,:date,*args)
29
+ end
30
+
31
+ # Specify which table columns should be typecasted to Time (or DateTime), e.g.:
32
+ #
33
+ # set_datetime_columns :created_date, :updated_date
34
+ def self.set_datetime_columns(*args)
35
+ connection.set_type_for_columns(table_name,:datetime,*args)
36
+ end
37
+
38
+ # Specify which table columns should be typecasted to boolean values +true+ or +false+, e.g.:
39
+ #
40
+ # set_boolean_columns :is_valid, :is_completed
41
+ def self.set_boolean_columns(*args)
42
+ connection.set_type_for_columns(table_name,:boolean,*args)
43
+ end
44
+
45
+ # Specify which table columns should be typecasted to integer values.
46
+ # Might be useful to force NUMBER(1) column to be integer and not boolean, or force NUMBER column without
47
+ # scale to be retrieved as integer and not decimal. Example:
48
+ #
49
+ # set_integer_columns :version_number, :object_identifier
50
+ def self.set_integer_columns(*args)
51
+ connection.set_type_for_columns(table_name,:integer,*args)
52
+ end
53
+
54
+ # Specify which table columns should be typecasted to string values.
55
+ # Might be useful to specify that columns should be string even if its name matches boolean column criteria.
56
+ #
57
+ # set_string_columns :active_flag
58
+ def self.set_string_columns(*args)
59
+ connection.set_type_for_columns(table_name,:string,*args)
60
+ end
61
+
62
+ # After setting large objects to empty, select the OCI8::LOB
63
+ # and write back the data.
64
+ if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR >= 1
65
+ after_update :enhanced_write_lobs
66
+ else
67
+ after_save :enhanced_write_lobs
68
+ end
69
+ def enhanced_write_lobs #:nodoc:
70
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
71
+ !(self.class.custom_create_method || self.class.custom_update_method)
72
+ connection.write_lobs(self.class.table_name, self.class, attributes)
73
+ end
74
+ end
75
+ private :enhanced_write_lobs
76
+
77
+ # Get table comment from schema definition.
78
+ def self.table_comment
79
+ connection.table_comment(self.table_name)
80
+ end
81
+
82
+ def self.virtual_columns
83
+ columns.select do |column|
84
+ column.respond_to?(:virtual?) && column.virtual?
85
+ end
86
+ end
87
+
88
+ if ActiveRecord::VERSION::MAJOR < 3
89
+ def attributes_with_quotes_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
90
+ virtual_column_names = self.class.virtual_columns.map(&:name)
91
+ attributes_with_quotes_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
92
+ end
93
+
94
+ alias_method_chain :attributes_with_quotes, :virtual_columns
95
+ else
96
+ def arel_attributes_values_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
97
+ virtual_column_names = self.class.virtual_columns.map(&:name)
98
+ arel_attributes_values_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
99
+ end
100
+
101
+ alias_method_chain :arel_attributes_values, :virtual_columns
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,136 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters #:nodoc:
3
+ class OracleEnhancedColumn < Column
4
+
5
+ attr_reader :table_name, :forced_column_type, :nchar, :virtual_column_data_default #:nodoc:
6
+
7
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil, virtual=false) #:nodoc:
8
+ @table_name = table_name
9
+ @forced_column_type = forced_column_type
10
+ @virtual = virtual
11
+ super(name, default, sql_type, null)
12
+ @virtual_column_data_default = default.inspect if virtual
13
+ # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)?
14
+ # Define only when needed as adapter "quote" method will check at first if instance variable is defined.
15
+ @nchar = true if @type == :string && sql_type[0,1] == 'N'
16
+ end
17
+
18
+ def type_cast(value) #:nodoc:
19
+ return OracleEnhancedColumn::string_to_raw(value) if type == :raw
20
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
21
+ super
22
+ end
23
+
24
+ def virtual?
25
+ @virtual
26
+ end
27
+
28
+ # convert something to a boolean
29
+ # added y as boolean value
30
+ def self.value_to_boolean(value) #:nodoc:
31
+ if value == true || value == false
32
+ value
33
+ elsif value.is_a?(String) && value.blank?
34
+ nil
35
+ else
36
+ %w(true t 1 y +).include?(value.to_s.downcase)
37
+ end
38
+ end
39
+
40
+ # convert Time or DateTime value to Date for :date columns
41
+ def self.string_to_date(string) #:nodoc:
42
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
43
+ super
44
+ end
45
+
46
+ # convert Date value to Time for :datetime columns
47
+ def self.string_to_time(string) #:nodoc:
48
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
49
+ super
50
+ end
51
+
52
+ # convert RAW column values back to byte strings.
53
+ def self.string_to_raw(string) #:nodoc:
54
+ string
55
+ end
56
+
57
+ # Get column comment from schema definition.
58
+ # Will work only if using default ActiveRecord connection.
59
+ def comment
60
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
61
+ end
62
+
63
+ private
64
+
65
+ def simplified_type(field_type)
66
+ forced_column_type ||
67
+ case field_type
68
+ when /decimal|numeric|number/i
69
+ if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
70
+ :boolean
71
+ elsif extract_scale(field_type) == 0 ||
72
+ # if column name is ID or ends with _ID
73
+ OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
74
+ :integer
75
+ else
76
+ :decimal
77
+ end
78
+ when /raw/i
79
+ :raw
80
+ when /char/i
81
+ if OracleEnhancedAdapter.emulate_booleans_from_strings &&
82
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
83
+ :boolean
84
+ else
85
+ :string
86
+ end
87
+ when /date/i
88
+ if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)
89
+ :date
90
+ else
91
+ :datetime
92
+ end
93
+ when /timestamp/i
94
+ :timestamp
95
+ when /time/i
96
+ :datetime
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ def guess_date_or_time(value)
103
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
104
+ Date.new(value.year, value.month, value.day) : value
105
+ end
106
+
107
+ class << self
108
+ protected
109
+
110
+ def fallback_string_to_date(string) #:nodoc:
111
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
112
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
113
+ end
114
+ super
115
+ end
116
+
117
+ def fallback_string_to_time(string) #:nodoc:
118
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
119
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
120
+ end
121
+ super
122
+ end
123
+
124
+ def string_to_date_or_time_using_format(string) #:nodoc:
125
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
126
+ return Time.parse("#{dt[:year]}-#{dt[:mon]}-#{dt[:mday]} #{dt[:hour]}:#{dt[:min]}:#{dt[:sec]}#{dt[:zone]}")
127
+ end
128
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format).to_date
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,119 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # interface independent methods
4
+ class OracleEnhancedConnection #:nodoc:
5
+
6
+ def self.create(config)
7
+ case ORACLE_ENHANCED_CONNECTION
8
+ when :oci
9
+ OracleEnhancedOCIConnection.new(config)
10
+ when :jdbc
11
+ OracleEnhancedJDBCConnection.new(config)
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ attr_reader :raw_connection
18
+
19
+ # Oracle column names by default are case-insensitive, but treated as upcase;
20
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
21
+ # their column names when creating Oracle tables, which makes then case-sensitive.
22
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
23
+ # camelCase column name. I imagine other dbs handle this different, since there's a
24
+ # unit test that's currently failing test_oci.
25
+ def oracle_downcase(column_name)
26
+ return nil if column_name.nil?
27
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
28
+ end
29
+
30
+ # Used always by JDBC connection as well by OCI connection when describing tables over database link
31
+ def describe(name)
32
+ name = name.to_s
33
+ if name.include?('@')
34
+ name, db_link = name.split('@')
35
+ default_owner = select_value("SELECT username FROM all_db_links WHERE db_link = '#{db_link.upcase}'")
36
+ db_link = "@#{db_link}"
37
+ else
38
+ db_link = nil
39
+ default_owner = @owner
40
+ end
41
+ real_name = OracleEnhancedAdapter.valid_table_name?(name) ? name.upcase : name
42
+ if real_name.include?('.')
43
+ table_owner, table_name = real_name.split('.')
44
+ else
45
+ table_owner, table_name = default_owner, real_name
46
+ end
47
+ sql = <<-SQL
48
+ SELECT owner, table_name, 'TABLE' name_type
49
+ FROM all_tables#{db_link}
50
+ WHERE owner = '#{table_owner}'
51
+ AND table_name = '#{table_name}'
52
+ UNION ALL
53
+ SELECT owner, view_name table_name, 'VIEW' name_type
54
+ FROM all_views#{db_link}
55
+ WHERE owner = '#{table_owner}'
56
+ AND view_name = '#{table_name}'
57
+ UNION ALL
58
+ SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type
59
+ FROM all_synonyms#{db_link}
60
+ WHERE owner = '#{table_owner}'
61
+ AND synonym_name = '#{table_name}'
62
+ UNION ALL
63
+ SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type
64
+ FROM all_synonyms#{db_link}
65
+ WHERE owner = 'PUBLIC'
66
+ AND synonym_name = '#{real_name}'
67
+ SQL
68
+ if result = select_one(sql)
69
+ case result['name_type']
70
+ when 'SYNONYM'
71
+ describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}#{db_link}")
72
+ else
73
+ db_link ? [result['owner'], result['table_name'], db_link] : [result['owner'], result['table_name']]
74
+ end
75
+ else
76
+ raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?}
77
+ end
78
+ end
79
+
80
+ # Returns a record hash with the column names as keys and column values
81
+ # as values.
82
+ def select_one(sql)
83
+ result = select(sql)
84
+ result.first if result
85
+ end
86
+
87
+ # Returns a single value from a record
88
+ def select_value(sql)
89
+ if result = select_one(sql)
90
+ result.values.first
91
+ end
92
+ end
93
+
94
+ # Returns an array of the values of the first column in a select:
95
+ # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
96
+ def select_values(sql, name = nil)
97
+ result = select(sql, name = nil)
98
+ result.map { |r| r.values.first }
99
+ end
100
+
101
+ end
102
+
103
+ class OracleEnhancedConnectionException < StandardError #:nodoc:
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ # if MRI or YARV
110
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
111
+ ORACLE_ENHANCED_CONNECTION = :oci
112
+ require 'active_record/connection_adapters/oracle_enhanced_oci_connection'
113
+ # if JRuby
114
+ elsif RUBY_ENGINE == 'jruby'
115
+ ORACLE_ENHANCED_CONNECTION = :jdbc
116
+ require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection'
117
+ else
118
+ raise "Unsupported Ruby engine #{RUBY_ENGINE}"
119
+ end
@@ -0,0 +1,328 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module OracleEnhancedContextIndex
4
+
5
+ # Define full text index with Oracle specific CONTEXT index type
6
+ #
7
+ # Oracle CONTEXT index by default supports full text indexing of one column.
8
+ # This method allows full text index creation also on several columns
9
+ # as well as indexing related table columns by generating stored procedure
10
+ # that concatenates all columns for indexing as well as generating trigger
11
+ # that will update main index column to trigger reindexing of record.
12
+ #
13
+ # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition
14
+ # and order by score of matched results.
15
+ #
16
+ # Options:
17
+ #
18
+ # * <tt>:name</tt>
19
+ # * <tt>:index_column</tt>
20
+ # * <tt>:index_column_trigger_on</tt>
21
+ # * <tt>:tablespace</tt>
22
+ # * <tt>:sync</tt> - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL').
23
+ # * <tt>:lexer</tt> - Lexer options (e.g. <tt>:type => 'BASIC_LEXER', :base_letter => true</tt>).
24
+ # * <tt>:transactional</tt> - When +true+, the CONTAINS operator will process inserted and updated rows.
25
+ #
26
+ # ===== Examples
27
+ #
28
+ # ====== Creating single column index
29
+ # add_context_index :posts, :title
30
+ # search with
31
+ # Post.contains(:title, 'word')
32
+ #
33
+ # ====== Creating index on several columns
34
+ # add_context_index :posts, [:title, :body]
35
+ # search with (use first column as argument for contains method but it will search in all index columns)
36
+ # Post.contains(:title, 'word')
37
+ #
38
+ # ====== Creating index on several columns with dummy index column and commit option
39
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
40
+ # search with
41
+ # Post.contains(:all_text, 'word')
42
+ #
43
+ # ====== Creating index with trigger option (will reindex when specified columns are updated)
44
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
45
+ # :index_column_trigger_on => [:created_at, :updated_at]
46
+ # search with
47
+ # Post.contains(:all_text, 'word')
48
+ #
49
+ # ====== Creating index on multiple tables
50
+ # add_context_index :posts,
51
+ # [:title, :body,
52
+ # # specify aliases always with AS keyword
53
+ # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
54
+ # ],
55
+ # :name => 'post_and_comments_index',
56
+ # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
57
+ # :sync => 'ON COMMIT'
58
+ # search in any table columns
59
+ # Post.contains(:all_text, 'word')
60
+ # search in specified column
61
+ # Post.contains(:all_text, "aaa within title")
62
+ # Post.contains(:all_text, "bbb within comment_author")
63
+ #
64
+ # ====== Creating index using lexer
65
+ # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... }
66
+ #
67
+ # ====== Creating transactional index (will reindex changed rows when querying)
68
+ # add_context_index :posts, :title, :transactional => true
69
+ #
70
+ def add_context_index(table_name, column_name, options = {})
71
+ self.all_schema_indexes = nil
72
+ column_names = Array(column_name)
73
+ index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names,
74
+ # CONEXT index name max length is 25
75
+ :identifier_max_length => 25)
76
+
77
+ quoted_column_name = quote_column_name(options[:index_column] || column_names.first)
78
+ if options[:index_column_trigger_on]
79
+ raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \
80
+ unless options[:index_column]
81
+ create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on])
82
+ end
83
+
84
+ sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
85
+ sql << " (#{quoted_column_name})"
86
+ sql << " INDEXTYPE IS CTXSYS.CONTEXT"
87
+ parameters = []
88
+ if column_names.size > 1
89
+ procedure_name = default_datastore_procedure(index_name)
90
+ datastore_name = default_datastore_name(index_name)
91
+ create_datastore_procedure(table_name, procedure_name, column_names, options)
92
+ create_datastore_preference(datastore_name, procedure_name)
93
+ parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP"
94
+ end
95
+ if options[:tablespace]
96
+ storage_name = default_storage_name(index_name)
97
+ create_storage_preference(storage_name, options[:tablespace])
98
+ parameters << "STORAGE #{storage_name}"
99
+ end
100
+ if options[:sync]
101
+ parameters << "SYNC(#{options[:sync]})"
102
+ end
103
+ if options[:lexer] && (lexer_type = options[:lexer][:type])
104
+ lexer_name = default_lexer_name(index_name)
105
+ (lexer_options = options[:lexer].dup).delete(:type)
106
+ create_lexer_preference(lexer_name, lexer_type, lexer_options)
107
+ parameters << "LEXER #{lexer_name}"
108
+ end
109
+ if options[:transactional]
110
+ parameters << "TRANSACTIONAL"
111
+ end
112
+ unless parameters.empty?
113
+ sql << " PARAMETERS ('#{parameters.join(' ')}')"
114
+ end
115
+ execute sql
116
+ end
117
+
118
+ # Drop full text index with Oracle specific CONTEXT index type
119
+ def remove_context_index(table_name, options = {})
120
+ self.all_schema_indexes = nil
121
+ unless Hash === options # if column names passed as argument
122
+ options = {:column => Array(options)}
123
+ end
124
+ index_name = options[:name] || index_name(table_name,
125
+ :column => options[:index_column] || options[:column], :identifier_max_length => 25)
126
+ execute "DROP INDEX #{index_name}"
127
+ drop_ctx_preference(default_datastore_name(index_name))
128
+ drop_ctx_preference(default_storage_name(index_name))
129
+ procedure_name = default_datastore_procedure(index_name)
130
+ execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
131
+ drop_index_column_trigger(index_name)
132
+ end
133
+
134
+ private
135
+
136
+ def create_datastore_procedure(table_name, procedure_name, column_names, options)
137
+ quoted_table_name = quote_table_name(table_name)
138
+ select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i }
139
+ select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') }
140
+ keys, selected_columns = parse_select_queries(select_queries)
141
+ quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)}
142
+ execute compress_lines(<<-SQL)
143
+ CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
144
+ (p_rowid IN ROWID,
145
+ p_clob IN OUT NOCOPY CLOB) IS
146
+ -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''}
147
+ #{
148
+ selected_columns.map do |cols|
149
+ cols.map do |col|
150
+ raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
151
+ "l_#{col} VARCHAR2(32767);\n"
152
+ end.join
153
+ end.join
154
+ } BEGIN
155
+ FOR r1 IN (
156
+ SELECT #{quoted_column_names.join(', ')}
157
+ FROM #{quoted_table_name}
158
+ WHERE #{quoted_table_name}.ROWID = p_rowid
159
+ ) LOOP
160
+ #{
161
+ (column_names.map do |col|
162
+ col = col.to_s
163
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
164
+ "IF LENGTH(r1.#{col}) > 0 THEN\n" <<
165
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
166
+ "END IF;\n" <<
167
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
168
+ end.join) <<
169
+ (selected_columns.zip(select_queries).map do |cols, query|
170
+ (cols.map do |col|
171
+ "l_#{col} := '';\n"
172
+ end.join) <<
173
+ "FOR r2 IN (\n" <<
174
+ query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" <<
175
+ (cols.map do |col|
176
+ "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
177
+ end.join) <<
178
+ "END LOOP;\n" <<
179
+ (cols.map do |col|
180
+ col = col.to_s
181
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
182
+ "IF LENGTH(l_#{col}) > 0 THEN\n" <<
183
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
184
+ "END IF;\n" <<
185
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
186
+ end.join)
187
+ end.join)
188
+ }
189
+ END LOOP;
190
+ END;
191
+ SQL
192
+ end
193
+
194
+ def parse_select_queries(select_queries)
195
+ keys = []
196
+ selected_columns = []
197
+ select_queries.each do |query|
198
+ # get primary or foreign keys like :id or :something_id
199
+ keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym})
200
+ select_part = query.scan(/^select\s.*\sfrom/i).first
201
+ selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first}
202
+ end
203
+ [keys.flatten.uniq, selected_columns]
204
+ end
205
+
206
+ def create_datastore_preference(datastore_name, procedure_name)
207
+ drop_ctx_preference(datastore_name)
208
+ execute <<-SQL
209
+ BEGIN
210
+ CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
211
+ CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
212
+ END;
213
+ SQL
214
+ end
215
+
216
+ def create_storage_preference(storage_name, tablespace)
217
+ drop_ctx_preference(storage_name)
218
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
219
+ ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE',
220
+ 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
221
+ default_clause = case clause
222
+ when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) '
223
+ when 'I_INDEX_CLAUSE'; 'COMPRESS 2 '
224
+ else ''
225
+ end
226
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
227
+ end
228
+ sql << "END;\n"
229
+ execute sql
230
+ end
231
+
232
+ def create_lexer_preference(lexer_name, lexer_type, options)
233
+ drop_ctx_preference(lexer_name)
234
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n"
235
+ options.each do |key, value|
236
+ plsql_value = case value
237
+ when String; "'#{value}'"
238
+ when true; "'YES'"
239
+ when false; "'NO'"
240
+ when nil; 'NULL'
241
+ else value
242
+ end
243
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
244
+ end
245
+ sql << "END;\n"
246
+ execute sql
247
+ end
248
+
249
+ def drop_ctx_preference(preference_name)
250
+ execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
251
+ end
252
+
253
+ def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
254
+ trigger_name = default_index_column_trigger_name(index_name)
255
+ columns = Array(index_column_source)
256
+ quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ')
257
+ execute compress_lines(<<-SQL)
258
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
259
+ BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
260
+ BEGIN
261
+ :new.#{quote_column_name(index_column)} := '1';
262
+ END;
263
+ SQL
264
+ end
265
+
266
+ def drop_index_column_trigger(index_name)
267
+ trigger_name = default_index_column_trigger_name(index_name)
268
+ execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
269
+ end
270
+
271
+ def default_datastore_procedure(index_name)
272
+ "#{index_name}_prc"
273
+ end
274
+
275
+ def default_datastore_name(index_name)
276
+ "#{index_name}_dst"
277
+ end
278
+
279
+ def default_storage_name(index_name)
280
+ "#{index_name}_sto"
281
+ end
282
+
283
+ def default_index_column_trigger_name(index_name)
284
+ "#{index_name}_trg"
285
+ end
286
+
287
+ def default_lexer_name(index_name)
288
+ "#{index_name}_lex"
289
+ end
290
+
291
+ module BaseClassMethods
292
+ # Declare that model table has context index defined.
293
+ # As a result <tt>contains</tt> class scope method is defined.
294
+ def has_context_index
295
+ extend ContextIndexClassMethods
296
+ end
297
+ end
298
+
299
+ module ContextIndexClassMethods
300
+ # Add context index condition.
301
+ case ::ActiveRecord::VERSION::MAJOR
302
+ when 3
303
+ def contains(column, query, options ={})
304
+ score_label = options[:label].to_i || 1
305
+ where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query).
306
+ order("SCORE(#{score_label}) DESC")
307
+ end
308
+ when 2
309
+ def contains(column, query, options ={})
310
+ score_label = options[:label].to_i || 1
311
+ scoped(:conditions => ["CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query],
312
+ :order => "SCORE(#{score_label}) DESC")
313
+ end
314
+ end
315
+ end
316
+
317
+ end
318
+
319
+ end
320
+ end
321
+
322
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
323
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex
324
+ end
325
+
326
+ ActiveRecord::Base.class_eval do
327
+ extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods
328
+ end