pmacs-activerecord-oracle_enhanced-adapter 1.4.2.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +52 -0
  3. data/History.md +284 -0
  4. data/License.txt +20 -0
  5. data/README.md +403 -0
  6. data/RUNNING_TESTS.md +45 -0
  7. data/Rakefile +59 -0
  8. data/VERSION +1 -0
  9. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1408 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +118 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +141 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +135 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +359 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +44 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +565 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +491 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +231 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +257 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +397 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +265 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +294 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
  29. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  30. data/lib/pmacs-activerecord-oracle_enhanced-adapter.rb +25 -0
  31. data/pmacs-activerecord-oracle_enhanced-adapter.gemspec +131 -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 +1376 -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 +438 -0
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1280 -0
  44. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +339 -0
  45. data/spec/spec_helper.rb +187 -0
  46. metadata +302 -0
@@ -0,0 +1,118 @@
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{|col| self.send(:"#{col.name}_changed?") && !self.class.readonly_attributes.to_a.include?(col.name)}
79
+ end
80
+ private :enhanced_write_lobs
81
+ private :record_changed_lobs
82
+
83
+ # Get table comment from schema definition.
84
+ def self.table_comment
85
+ connection.table_comment(self.table_name)
86
+ end
87
+
88
+ def self.lob_columns
89
+ columns.select do |column|
90
+ column.respond_to?(:lob?) && column.lob?
91
+ end
92
+ end
93
+
94
+ def self.virtual_columns
95
+ columns.select do |column|
96
+ column.respond_to?(:virtual?) && column.virtual?
97
+ end
98
+ end
99
+
100
+ if ActiveRecord::VERSION::MAJOR < 3
101
+ def attributes_with_quotes_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
102
+ virtual_column_names = self.class.virtual_columns.map(&:name)
103
+ attributes_with_quotes_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
104
+ end
105
+
106
+ alias_method_chain :attributes_with_quotes, :virtual_columns
107
+ else
108
+ def arel_attributes_values_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
109
+ virtual_column_names = self.class.virtual_columns.map(&:name)
110
+ arel_attributes_values_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_column_names)
111
+ end
112
+
113
+ alias_method_chain :arel_attributes_values, :virtual_columns
114
+ end
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,141 @@
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
+ @virtual_column_data_default = default.inspect if virtual
12
+ default = nil if virtual
13
+ super(name, default, sql_type, null)
14
+ # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)?
15
+ # Define only when needed as adapter "quote" method will check at first if instance variable is defined.
16
+ @nchar = true if @type == :string && sql_type[0,1] == 'N'
17
+ end
18
+
19
+ def type_cast(value) #:nodoc:
20
+ return OracleEnhancedColumn::string_to_raw(value) if type == :raw
21
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
22
+ super
23
+ end
24
+
25
+ def virtual?
26
+ @virtual
27
+ end
28
+
29
+ def lob?
30
+ self.sql_type =~ /LOB$/i
31
+ end
32
+
33
+ # convert something to a boolean
34
+ # added y as boolean value
35
+ def self.value_to_boolean(value) #:nodoc:
36
+ if value == true || value == false
37
+ value
38
+ elsif value.is_a?(String) && value.blank?
39
+ nil
40
+ else
41
+ %w(true t 1 y +).include?(value.to_s.downcase)
42
+ end
43
+ end
44
+
45
+ # convert Time or DateTime value to Date for :date columns
46
+ def self.string_to_date(string) #:nodoc:
47
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
48
+ super
49
+ end
50
+
51
+ # convert Date value to Time for :datetime columns
52
+ def self.string_to_time(string) #:nodoc:
53
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
54
+ super
55
+ end
56
+
57
+ # convert RAW column values back to byte strings.
58
+ def self.string_to_raw(string) #:nodoc:
59
+ string
60
+ end
61
+
62
+ # Get column comment from schema definition.
63
+ # Will work only if using default ActiveRecord connection.
64
+ def comment
65
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
66
+ end
67
+
68
+ private
69
+
70
+ def simplified_type(field_type)
71
+ forced_column_type ||
72
+ case field_type
73
+ when /decimal|numeric|number/i
74
+ if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
75
+ :boolean
76
+ elsif extract_scale(field_type) == 0 ||
77
+ # if column name is ID or ends with _ID
78
+ OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
79
+ :integer
80
+ else
81
+ :decimal
82
+ end
83
+ when /raw/i
84
+ :raw
85
+ when /char/i
86
+ if OracleEnhancedAdapter.emulate_booleans_from_strings &&
87
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
88
+ :boolean
89
+ else
90
+ :string
91
+ end
92
+ when /date/i
93
+ if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)
94
+ :date
95
+ else
96
+ :datetime
97
+ end
98
+ when /timestamp/i
99
+ :timestamp
100
+ when /time/i
101
+ :datetime
102
+ else
103
+ super
104
+ end
105
+ end
106
+
107
+ def guess_date_or_time(value)
108
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
109
+ Date.new(value.year, value.month, value.day) : value
110
+ end
111
+
112
+ class << self
113
+ protected
114
+
115
+ def fallback_string_to_date(string) #:nodoc:
116
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
117
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
118
+ end
119
+ super
120
+ end
121
+
122
+ def fallback_string_to_time(string) #:nodoc:
123
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
124
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
125
+ end
126
+ super
127
+ end
128
+
129
+ def string_to_date_or_time_using_format(string) #:nodoc:
130
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
131
+ return Time.parse("#{dt[:year]}-#{dt[:mon]}-#{dt[:mday]} #{dt[:hour]}:#{dt[:min]}:#{dt[:sec]}#{dt[:zone]}")
132
+ end
133
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format).to_date
134
+ end
135
+
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,135 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # interface independent methods
4
+ class OracleEnhancedConnection #:nodoc:
5
+
6
+ def self.create(config)
7
+ connection = create_connection(config)
8
+ unless connection.nil?
9
+ connection.after_initialize(config)
10
+ end
11
+ connection
12
+ end
13
+
14
+ def after_initialize(config)
15
+ schema = config[:schema]
16
+ unless schema.blank?
17
+ exec "alter session set current_schema = #{schema}"
18
+ @owner = schema.upcase
19
+ end
20
+ end
21
+
22
+ attr_reader :raw_connection
23
+
24
+ # Oracle column names by default are case-insensitive, but treated as upcase;
25
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
26
+ # their column names when creating Oracle tables, which makes then case-sensitive.
27
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
28
+ # camelCase column name. I imagine other dbs handle this different, since there's a
29
+ # unit test that's currently failing test_oci.
30
+ def oracle_downcase(column_name)
31
+ return nil if column_name.nil?
32
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
33
+ end
34
+
35
+ # Used always by JDBC connection as well by OCI connection when describing tables over database link
36
+ def describe(name)
37
+ name = name.to_s
38
+ if name.include?('@')
39
+ name, db_link = name.split('@')
40
+ default_owner = select_value("SELECT username FROM all_db_links WHERE db_link = '#{db_link.upcase}'")
41
+ db_link = "@#{db_link}"
42
+ else
43
+ db_link = nil
44
+ default_owner = @owner
45
+ end
46
+ real_name = OracleEnhancedAdapter.valid_table_name?(name) ? name.upcase : name
47
+ if real_name.include?('.')
48
+ table_owner, table_name = real_name.split('.')
49
+ else
50
+ table_owner, table_name = default_owner, real_name
51
+ end
52
+ sql = <<-SQL
53
+ SELECT owner, table_name, 'TABLE' name_type
54
+ FROM all_tables#{db_link}
55
+ WHERE owner = '#{table_owner}'
56
+ AND table_name = '#{table_name}'
57
+ UNION ALL
58
+ SELECT owner, view_name table_name, 'VIEW' name_type
59
+ FROM all_views#{db_link}
60
+ WHERE owner = '#{table_owner}'
61
+ AND view_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 = '#{table_owner}'
66
+ AND synonym_name = '#{table_name}'
67
+ UNION ALL
68
+ SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type
69
+ FROM all_synonyms#{db_link}
70
+ WHERE owner = 'PUBLIC'
71
+ AND synonym_name = '#{real_name}'
72
+ SQL
73
+ if result = select_one(sql)
74
+ case result['name_type']
75
+ when 'SYNONYM'
76
+ describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}#{db_link}")
77
+ else
78
+ db_link ? [result['owner'], result['table_name'], db_link] : [result['owner'], result['table_name']]
79
+ end
80
+ else
81
+ raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?}
82
+ end
83
+ end
84
+
85
+ # Returns a record hash with the column names as keys and column values
86
+ # as values.
87
+ def select_one(sql)
88
+ result = select(sql)
89
+ result.first if result
90
+ end
91
+
92
+ # Returns a single value from a record
93
+ def select_value(sql)
94
+ if result = select_one(sql)
95
+ result.values.first
96
+ end
97
+ end
98
+
99
+ # Returns an array of the values of the first column in a select:
100
+ # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
101
+ def select_values(sql, name = nil)
102
+ result = select(sql, name = nil)
103
+ result.map { |r| r.values.first }
104
+ end
105
+
106
+
107
+ protected
108
+ def self.create_connection(config)
109
+ case ORACLE_ENHANCED_CONNECTION
110
+ when :oci
111
+ OracleEnhancedOCIConnection.new(config)
112
+ when :jdbc
113
+ OracleEnhancedJDBCConnection.new(config)
114
+ else
115
+ nil
116
+ end
117
+ end
118
+ end
119
+
120
+ class OracleEnhancedConnectionException < StandardError #:nodoc:
121
+ end
122
+ end
123
+ end
124
+
125
+ # if MRI or YARV
126
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
127
+ ORACLE_ENHANCED_CONNECTION = :oci
128
+ require 'active_record/connection_adapters/oracle_enhanced_oci_connection'
129
+ # if JRuby
130
+ elsif RUBY_ENGINE == 'jruby'
131
+ ORACLE_ENHANCED_CONNECTION = :jdbc
132
+ require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection'
133
+ else
134
+ raise "Unsupported Ruby engine #{RUBY_ENGINE}"
135
+ 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