activerecord-oracle_enhanced-adapter-with-schema 0.0.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 (46) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +52 -0
  3. data/History.md +301 -0
  4. data/License.txt +20 -0
  5. data/README.md +123 -0
  6. data/RUNNING_TESTS.md +45 -0
  7. data/Rakefile +59 -0
  8. data/VERSION +1 -0
  9. data/activerecord-oracle_enhanced-adapter-with-schema.gemspec +130 -0
  10. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1399 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +121 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +146 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +119 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +359 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +46 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +565 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +494 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +227 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +260 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +428 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +258 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +294 -0
  29. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
  30. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  31. data/lib/activerecord-oracle_enhanced-adapter-with-schema.rb +25 -0
  32. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +778 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +332 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +427 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
  36. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
  37. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1388 -0
  38. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
  39. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +141 -0
  40. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  41. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +378 -0
  42. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +440 -0
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1385 -0
  44. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +339 -0
  45. data/spec/spec_helper.rb +189 -0
  46. metadata +260 -0
@@ -0,0 +1,121 @@
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
+ before_update :record_changed_lobs
66
+ after_update :enhanced_write_lobs
67
+ else
68
+ before_update :record_changed_lobs
69
+ after_save :enhanced_write_lobs
70
+ end
71
+ def enhanced_write_lobs #:nodoc:
72
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
73
+ !(self.class.custom_create_method || self.class.custom_update_method)
74
+ connection.write_lobs(self.class.table_name, self.class, attributes, @changed_lob_columns || self.class.lob_columns)
75
+ end
76
+ end
77
+ def record_changed_lobs
78
+ @changed_lob_columns = self.class.lob_columns.select do |col|
79
+ self.class.serialized_attributes.keys.include?(col.name) ||
80
+ (self.send(:"#{col.name}_changed?") && !self.class.readonly_attributes.to_a.include?(col.name))
81
+ end
82
+ end
83
+ private :enhanced_write_lobs
84
+ private :record_changed_lobs
85
+
86
+ # Get table comment from schema definition.
87
+ def self.table_comment
88
+ connection.table_comment(self.table_name)
89
+ end
90
+
91
+ def self.lob_columns
92
+ columns.select do |column|
93
+ column.respond_to?(:lob?) && column.lob?
94
+ end
95
+ end
96
+
97
+ def self.virtual_columns
98
+ columns.select do |column|
99
+ column.respond_to?(:virtual?) && column.virtual?
100
+ end
101
+ end
102
+
103
+ if ActiveRecord::VERSION::MAJOR < 3
104
+ def attributes_with_quotes_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
105
+ virtual_column_names = self.class.virtual_columns.map(&:name)
106
+ attributes_with_quotes_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
107
+ end
108
+
109
+ alias_method_chain :attributes_with_quotes, :virtual_columns
110
+ else
111
+ def arel_attributes_values_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
112
+ virtual_column_names = self.class.virtual_columns.map(&:name)
113
+ arel_attributes_values_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
114
+ end
115
+
116
+ alias_method_chain :arel_attributes_values, :virtual_columns
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,146 @@
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, :returning_id #:nodoc:
6
+
7
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil, virtual=false, returning_id=false) #:nodoc:
8
+ @table_name = table_name
9
+ @forced_column_type = forced_column_type
10
+ @virtual = virtual
11
+ @virtual_column_data_default = default.inspect if virtual
12
+ @returning_id = returning_id
13
+ default = nil if virtual
14
+ super(name, default, sql_type, null)
15
+ # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)?
16
+ # Define only when needed as adapter "quote" method will check at first if instance variable is defined.
17
+ @nchar = true if @type == :string && sql_type[0,1] == 'N'
18
+ end
19
+
20
+ def type_cast(value) #:nodoc:
21
+ return OracleEnhancedColumn::string_to_raw(value) if type == :raw
22
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
23
+ super
24
+ end
25
+
26
+ def virtual?
27
+ @virtual
28
+ end
29
+
30
+ def returning_id?
31
+ @returning_id
32
+ end
33
+
34
+ def lob?
35
+ self.sql_type =~ /LOB$/i
36
+ end
37
+
38
+ # convert something to a boolean
39
+ # added y as boolean value
40
+ def self.value_to_boolean(value) #:nodoc:
41
+ if value == true || value == false
42
+ value
43
+ elsif value.is_a?(String) && value.blank?
44
+ nil
45
+ else
46
+ %w(true t 1 y +).include?(value.to_s.downcase)
47
+ end
48
+ end
49
+
50
+ # convert Time or DateTime value to Date for :date columns
51
+ def self.string_to_date(string) #:nodoc:
52
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
53
+ super
54
+ end
55
+
56
+ # convert Date value to Time for :datetime columns
57
+ def self.string_to_time(string) #:nodoc:
58
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
59
+ super
60
+ end
61
+
62
+ # convert RAW column values back to byte strings.
63
+ def self.string_to_raw(string) #:nodoc:
64
+ string
65
+ end
66
+
67
+ # Get column comment from schema definition.
68
+ # Will work only if using default ActiveRecord connection.
69
+ def comment
70
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
71
+ end
72
+
73
+ private
74
+
75
+ def simplified_type(field_type)
76
+ forced_column_type ||
77
+ case field_type
78
+ when /decimal|numeric|number/i
79
+ if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
80
+ :boolean
81
+ elsif extract_scale(field_type) == 0 ||
82
+ # if column name is ID or ends with _ID
83
+ OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
84
+ :integer
85
+ else
86
+ :decimal
87
+ end
88
+ when /raw/i
89
+ :raw
90
+ when /char/i
91
+ if OracleEnhancedAdapter.emulate_booleans_from_strings &&
92
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
93
+ :boolean
94
+ else
95
+ :string
96
+ end
97
+ when /date/i
98
+ if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)
99
+ :date
100
+ else
101
+ :datetime
102
+ end
103
+ when /timestamp/i
104
+ :timestamp
105
+ when /time/i
106
+ :datetime
107
+ else
108
+ super
109
+ end
110
+ end
111
+
112
+ def guess_date_or_time(value)
113
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
114
+ Date.new(value.year, value.month, value.day) : value
115
+ end
116
+
117
+ class << self
118
+ protected
119
+
120
+ def fallback_string_to_date(string) #:nodoc:
121
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
122
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
123
+ end
124
+ super
125
+ end
126
+
127
+ def fallback_string_to_time(string) #:nodoc:
128
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
129
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
130
+ end
131
+ super
132
+ end
133
+
134
+ def string_to_date_or_time_using_format(string) #:nodoc:
135
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
136
+ return Time.parse("#{dt[:year]}-#{dt[:mon]}-#{dt[:mday]} #{dt[:hour]}:#{dt[:min]}:#{dt[:sec]}#{dt[:zone]}")
137
+ end
138
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format).to_date
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ 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,359 @@
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>:wordlist</tt> - Wordlist options (e.g. <tt>:type => 'BASIC_WORDLIST', :prefix_index => true</tt>).
25
+ # * <tt>:transactional</tt> - When +true+, the CONTAINS operator will process inserted and updated rows.
26
+ #
27
+ # ===== Examples
28
+ #
29
+ # ====== Creating single column index
30
+ # add_context_index :posts, :title
31
+ # search with
32
+ # Post.contains(:title, 'word')
33
+ #
34
+ # ====== Creating index on several columns
35
+ # add_context_index :posts, [:title, :body]
36
+ # search with (use first column as argument for contains method but it will search in all index columns)
37
+ # Post.contains(:title, 'word')
38
+ #
39
+ # ====== Creating index on several columns with dummy index column and commit option
40
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
41
+ # search with
42
+ # Post.contains(:all_text, 'word')
43
+ #
44
+ # ====== Creating index with trigger option (will reindex when specified columns are updated)
45
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
46
+ # :index_column_trigger_on => [:created_at, :updated_at]
47
+ # search with
48
+ # Post.contains(:all_text, 'word')
49
+ #
50
+ # ====== Creating index on multiple tables
51
+ # add_context_index :posts,
52
+ # [:title, :body,
53
+ # # specify aliases always with AS keyword
54
+ # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
55
+ # ],
56
+ # :name => 'post_and_comments_index',
57
+ # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
58
+ # :sync => 'ON COMMIT'
59
+ # search in any table columns
60
+ # Post.contains(:all_text, 'word')
61
+ # search in specified column
62
+ # Post.contains(:all_text, "aaa within title")
63
+ # Post.contains(:all_text, "bbb within comment_author")
64
+ #
65
+ # ====== Creating index using lexer
66
+ # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... }
67
+ #
68
+ # ====== Creating index using wordlist
69
+ # add_context_index :posts, :title, :wordlist => { :type => 'BASIC_WORDLIST', :prefix_index => true, ... }
70
+ #
71
+ # ====== Creating transactional index (will reindex changed rows when querying)
72
+ # add_context_index :posts, :title, :transactional => true
73
+ #
74
+ def add_context_index(table_name, column_name, options = {})
75
+ self.all_schema_indexes = nil
76
+ column_names = Array(column_name)
77
+ index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names,
78
+ # CONEXT index name max length is 25
79
+ :identifier_max_length => 25)
80
+
81
+ quoted_column_name = quote_column_name(options[:index_column] || column_names.first)
82
+ if options[:index_column_trigger_on]
83
+ raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \
84
+ unless options[:index_column]
85
+ create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on])
86
+ end
87
+
88
+ sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
89
+ sql << " (#{quoted_column_name})"
90
+ sql << " INDEXTYPE IS CTXSYS.CONTEXT"
91
+ parameters = []
92
+ if column_names.size > 1
93
+ procedure_name = default_datastore_procedure(index_name)
94
+ datastore_name = default_datastore_name(index_name)
95
+ create_datastore_procedure(table_name, procedure_name, column_names, options)
96
+ create_datastore_preference(datastore_name, procedure_name)
97
+ parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP"
98
+ end
99
+ if options[:tablespace]
100
+ storage_name = default_storage_name(index_name)
101
+ create_storage_preference(storage_name, options[:tablespace])
102
+ parameters << "STORAGE #{storage_name}"
103
+ end
104
+ if options[:sync]
105
+ parameters << "SYNC(#{options[:sync]})"
106
+ end
107
+ if options[:lexer] && (lexer_type = options[:lexer][:type])
108
+ lexer_name = default_lexer_name(index_name)
109
+ (lexer_options = options[:lexer].dup).delete(:type)
110
+ create_lexer_preference(lexer_name, lexer_type, lexer_options)
111
+ parameters << "LEXER #{lexer_name}"
112
+ end
113
+ if options[:wordlist] && (wordlist_type = options[:wordlist][:type])
114
+ wordlist_name = default_wordlist_name(index_name)
115
+ (wordlist_options = options[:wordlist].dup).delete(:type)
116
+ create_wordlist_preference(wordlist_name, wordlist_type, wordlist_options)
117
+ parameters << "WORDLIST #{wordlist_name}"
118
+ end
119
+ if options[:transactional]
120
+ parameters << "TRANSACTIONAL"
121
+ end
122
+ unless parameters.empty?
123
+ sql << " PARAMETERS ('#{parameters.join(' ')}')"
124
+ end
125
+ execute sql
126
+ end
127
+
128
+ # Drop full text index with Oracle specific CONTEXT index type
129
+ def remove_context_index(table_name, options = {})
130
+ self.all_schema_indexes = nil
131
+ unless Hash === options # if column names passed as argument
132
+ options = {:column => Array(options)}
133
+ end
134
+ index_name = options[:name] || index_name(table_name,
135
+ :column => options[:index_column] || options[:column], :identifier_max_length => 25)
136
+ execute "DROP INDEX #{index_name}"
137
+ drop_ctx_preference(default_datastore_name(index_name))
138
+ drop_ctx_preference(default_storage_name(index_name))
139
+ procedure_name = default_datastore_procedure(index_name)
140
+ execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
141
+ drop_index_column_trigger(index_name)
142
+ end
143
+
144
+ private
145
+
146
+ def create_datastore_procedure(table_name, procedure_name, column_names, options)
147
+ quoted_table_name = quote_table_name(table_name)
148
+ select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i }
149
+ select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') }
150
+ keys, selected_columns = parse_select_queries(select_queries)
151
+ quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)}
152
+ execute compress_lines(<<-SQL)
153
+ CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
154
+ (p_rowid IN ROWID,
155
+ p_clob IN OUT NOCOPY CLOB) IS
156
+ -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''}
157
+ #{
158
+ selected_columns.map do |cols|
159
+ cols.map do |col|
160
+ raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
161
+ "l_#{col} VARCHAR2(32767);\n"
162
+ end.join
163
+ end.join
164
+ } BEGIN
165
+ FOR r1 IN (
166
+ SELECT #{quoted_column_names.join(', ')}
167
+ FROM #{quoted_table_name}
168
+ WHERE #{quoted_table_name}.ROWID = p_rowid
169
+ ) LOOP
170
+ #{
171
+ (column_names.map do |col|
172
+ col = col.to_s
173
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
174
+ "IF LENGTH(r1.#{col}) > 0 THEN\n" <<
175
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
176
+ "END IF;\n" <<
177
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
178
+ end.join) <<
179
+ (selected_columns.zip(select_queries).map do |cols, query|
180
+ (cols.map do |col|
181
+ "l_#{col} := '';\n"
182
+ end.join) <<
183
+ "FOR r2 IN (\n" <<
184
+ query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" <<
185
+ (cols.map do |col|
186
+ "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
187
+ end.join) <<
188
+ "END LOOP;\n" <<
189
+ (cols.map do |col|
190
+ col = col.to_s
191
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
192
+ "IF LENGTH(l_#{col}) > 0 THEN\n" <<
193
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
194
+ "END IF;\n" <<
195
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
196
+ end.join)
197
+ end.join)
198
+ }
199
+ END LOOP;
200
+ END;
201
+ SQL
202
+ end
203
+
204
+ def parse_select_queries(select_queries)
205
+ keys = []
206
+ selected_columns = []
207
+ select_queries.each do |query|
208
+ # get primary or foreign keys like :id or :something_id
209
+ keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym})
210
+ select_part = query.scan(/^select\s.*\sfrom/i).first
211
+ selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first}
212
+ end
213
+ [keys.flatten.uniq, selected_columns]
214
+ end
215
+
216
+ def create_datastore_preference(datastore_name, procedure_name)
217
+ drop_ctx_preference(datastore_name)
218
+ execute <<-SQL
219
+ BEGIN
220
+ CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
221
+ CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
222
+ END;
223
+ SQL
224
+ end
225
+
226
+ def create_storage_preference(storage_name, tablespace)
227
+ drop_ctx_preference(storage_name)
228
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
229
+ ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE',
230
+ 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
231
+ default_clause = case clause
232
+ when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) '
233
+ when 'I_INDEX_CLAUSE'; 'COMPRESS 2 '
234
+ else ''
235
+ end
236
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
237
+ end
238
+ sql << "END;\n"
239
+ execute sql
240
+ end
241
+
242
+ def create_lexer_preference(lexer_name, lexer_type, options)
243
+ drop_ctx_preference(lexer_name)
244
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n"
245
+ options.each do |key, value|
246
+ plsql_value = case value
247
+ when String; "'#{value}'"
248
+ when true; "'YES'"
249
+ when false; "'NO'"
250
+ when nil; 'NULL'
251
+ else value
252
+ end
253
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
254
+ end
255
+ sql << "END;\n"
256
+ execute sql
257
+ end
258
+
259
+ def create_wordlist_preference(wordlist_name, wordlist_type, options)
260
+ drop_ctx_preference(wordlist_name)
261
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n"
262
+ options.each do |key, value|
263
+ plsql_value = case value
264
+ when String; "'#{value}'"
265
+ when true; "'YES'"
266
+ when false; "'NO'"
267
+ when nil; 'NULL'
268
+ else value
269
+ end
270
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n"
271
+ end
272
+ sql << "END;\n"
273
+ execute sql
274
+ end
275
+
276
+ def drop_ctx_preference(preference_name)
277
+ execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
278
+ end
279
+
280
+ def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
281
+ trigger_name = default_index_column_trigger_name(index_name)
282
+ columns = Array(index_column_source)
283
+ quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ')
284
+ execute compress_lines(<<-SQL)
285
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
286
+ BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
287
+ BEGIN
288
+ :new.#{quote_column_name(index_column)} := '1';
289
+ END;
290
+ SQL
291
+ end
292
+
293
+ def drop_index_column_trigger(index_name)
294
+ trigger_name = default_index_column_trigger_name(index_name)
295
+ execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
296
+ end
297
+
298
+ def default_datastore_procedure(index_name)
299
+ "#{index_name}_prc"
300
+ end
301
+
302
+ def default_datastore_name(index_name)
303
+ "#{index_name}_dst"
304
+ end
305
+
306
+ def default_storage_name(index_name)
307
+ "#{index_name}_sto"
308
+ end
309
+
310
+ def default_index_column_trigger_name(index_name)
311
+ "#{index_name}_trg"
312
+ end
313
+
314
+ def default_lexer_name(index_name)
315
+ "#{index_name}_lex"
316
+ end
317
+
318
+ def default_wordlist_name(index_name)
319
+ "#{index_name}_wl"
320
+ end
321
+
322
+ module BaseClassMethods
323
+ # Declare that model table has context index defined.
324
+ # As a result <tt>contains</tt> class scope method is defined.
325
+ def has_context_index
326
+ extend ContextIndexClassMethods
327
+ end
328
+ end
329
+
330
+ module ContextIndexClassMethods
331
+ # Add context index condition.
332
+ case ::ActiveRecord::VERSION::MAJOR
333
+ when 3
334
+ def contains(column, query, options ={})
335
+ score_label = options[:label].to_i || 1
336
+ where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query).
337
+ order("SCORE(#{score_label}) DESC")
338
+ end
339
+ when 2
340
+ def contains(column, query, options ={})
341
+ score_label = options[:label].to_i || 1
342
+ scoped(:conditions => ["CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query],
343
+ :order => "SCORE(#{score_label}) DESC")
344
+ end
345
+ end
346
+ end
347
+
348
+ end
349
+
350
+ end
351
+ end
352
+
353
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
354
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex
355
+ end
356
+
357
+ ActiveRecord::Base.class_eval do
358
+ extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods
359
+ end