ctreatma-activerecord-oracle_enhanced-adapter 1.4.1.1

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 (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