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.
- data/.rspec +2 -0
- data/Gemfile +52 -0
- data/History.md +301 -0
- data/License.txt +20 -0
- data/README.md +123 -0
- data/RUNNING_TESTS.md +45 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/activerecord-oracle_enhanced-adapter-with-schema.gemspec +130 -0
- data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
- data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1399 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +121 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +146 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +119 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +359 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +46 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +565 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +494 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +227 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +260 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +428 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +258 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +294 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
- data/lib/activerecord-oracle_enhanced-adapter-with-schema.rb +25 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +778 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +332 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +427 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1388 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +141 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +378 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +440 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1385 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +339 -0
- data/spec/spec_helper.rb +189 -0
- 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
|