activerecord 1.13.2 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +452 -10
- data/RUNNING_UNIT_TESTS +1 -1
- data/lib/active_record.rb +5 -2
- data/lib/active_record/acts/list.rb +1 -1
- data/lib/active_record/acts/tree.rb +29 -25
- data/lib/active_record/aggregations.rb +3 -2
- data/lib/active_record/associations.rb +783 -337
- data/lib/active_record/associations/association_collection.rb +7 -12
- data/lib/active_record/associations/association_proxy.rb +62 -24
- data/lib/active_record/associations/belongs_to_association.rb +27 -46
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
- data/lib/active_record/associations/has_many_association.rb +61 -56
- data/lib/active_record/associations/has_many_through_association.rb +144 -0
- data/lib/active_record/associations/has_one_association.rb +22 -16
- data/lib/active_record/base.rb +482 -182
- data/lib/active_record/calculations.rb +225 -0
- data/lib/active_record/callbacks.rb +7 -7
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
- data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
- data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
- data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
- data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
- data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
- data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
- data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
- data/lib/active_record/fixtures.rb +42 -17
- data/lib/active_record/locking.rb +36 -15
- data/lib/active_record/migration.rb +111 -8
- data/lib/active_record/observer.rb +25 -1
- data/lib/active_record/reflection.rb +103 -41
- data/lib/active_record/schema.rb +2 -2
- data/lib/active_record/schema_dumper.rb +55 -18
- data/lib/active_record/timestamp.rb +6 -6
- data/lib/active_record/validations.rb +65 -40
- data/lib/active_record/vendor/db2.rb +10 -5
- data/lib/active_record/vendor/simple.rb +693 -702
- data/lib/active_record/version.rb +2 -2
- data/rakefile +4 -4
- data/test/aaa_create_tables_test.rb +25 -6
- data/test/abstract_unit.rb +39 -1
- data/test/adapter_test.rb +31 -4
- data/test/associations_cascaded_eager_loading_test.rb +106 -0
- data/test/associations_go_eager_test.rb +85 -16
- data/test/associations_join_model_test.rb +338 -0
- data/test/associations_test.rb +129 -50
- data/test/base_test.rb +204 -49
- data/test/binary_test.rb +1 -1
- data/test/calculations_test.rb +169 -0
- data/test/callbacks_test.rb +5 -23
- data/test/class_inheritable_attributes_test.rb +1 -1
- data/test/column_alias_test.rb +1 -1
- data/test/connections/native_mysql/connection.rb +1 -0
- data/test/connections/native_openbase/connection.rb +22 -0
- data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
- data/test/connections/native_sqlite/connection.rb +1 -1
- data/test/connections/native_sqlite3/connection.rb +1 -0
- data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
- data/test/connections/native_sybase/connection.rb +24 -0
- data/test/defaults_test.rb +18 -0
- data/test/deprecated_associations_test.rb +2 -2
- data/test/deprecated_finder_test.rb +0 -6
- data/test/finder_test.rb +26 -23
- data/test/fixtures/accounts.yml +10 -0
- data/test/fixtures/author.rb +31 -6
- data/test/fixtures/author_favorites.yml +4 -0
- data/test/fixtures/categories/special_categories.yml +9 -0
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/fixtures/categories_posts.yml +4 -0
- data/test/fixtures/categorization.rb +5 -0
- data/test/fixtures/categorizations.yml +11 -0
- data/test/fixtures/category.rb +6 -0
- data/test/fixtures/company.rb +17 -5
- data/test/fixtures/company_in_module.rb +19 -5
- data/test/fixtures/db_definitions/db2.drop.sql +3 -0
- data/test/fixtures/db_definitions/db2.sql +121 -100
- data/test/fixtures/db_definitions/db22.sql +2 -2
- data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
- data/test/fixtures/db_definitions/firebird.sql +26 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
- data/test/fixtures/db_definitions/mysql.sql +21 -1
- data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase.sql +282 -0
- data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase2.sql +7 -0
- data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
- data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
- data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
- data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +22 -1
- data/test/fixtures/db_definitions/schema.rb +32 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
- data/test/fixtures/db_definitions/sqlite.sql +18 -0
- data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
- data/test/fixtures/db_definitions/sqlserver.sql +23 -3
- data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
- data/test/fixtures/db_definitions/sybase.sql +204 -0
- data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
- data/test/fixtures/db_definitions/sybase2.sql +5 -0
- data/test/fixtures/developers.yml +6 -1
- data/test/fixtures/developers_projects.yml +4 -0
- data/test/fixtures/funny_jokes.yml +14 -0
- data/test/fixtures/joke.rb +6 -0
- data/test/fixtures/legacy_thing.rb +3 -0
- data/test/fixtures/legacy_things.yml +3 -0
- data/test/fixtures/mixin.rb +1 -1
- data/test/fixtures/person.rb +4 -1
- data/test/fixtures/post.rb +26 -1
- data/test/fixtures/project.rb +1 -0
- data/test/fixtures/reader.rb +4 -0
- data/test/fixtures/readers.yml +4 -0
- data/test/fixtures/reply.rb +2 -1
- data/test/fixtures/tag.rb +5 -0
- data/test/fixtures/tagging.rb +6 -0
- data/test/fixtures/taggings.yml +18 -0
- data/test/fixtures/tags.yml +7 -0
- data/test/fixtures/tasks.yml +2 -2
- data/test/fixtures/topic.rb +2 -2
- data/test/fixtures/topics.yml +1 -0
- data/test/fixtures_test.rb +47 -13
- data/test/inheritance_test.rb +2 -2
- data/test/locking_test.rb +15 -1
- data/test/method_scoping_test.rb +248 -13
- data/test/migration_test.rb +68 -11
- data/test/mixin_nested_set_test.rb +1 -1
- data/test/modules_test.rb +6 -1
- data/test/readonly_test.rb +1 -1
- data/test/reflection_test.rb +63 -9
- data/test/schema_dumper_test.rb +41 -0
- data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
- data/test/threaded_connections_test.rb +10 -0
- data/test/unconnected_test.rb +12 -5
- data/test/validations_test.rb +197 -10
- metadata +295 -260
- data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
- data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Base
|
5
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
6
|
+
def self.openbase_connection(config) # :nodoc:
|
7
|
+
require_library_or_gem 'openbase' unless self.class.const_defined?(:OpenBase)
|
8
|
+
|
9
|
+
config = config.symbolize_keys
|
10
|
+
host = config[:host]
|
11
|
+
username = config[:username].to_s
|
12
|
+
password = config[:password].to_s
|
13
|
+
|
14
|
+
|
15
|
+
if config.has_key?(:database)
|
16
|
+
database = config[:database]
|
17
|
+
else
|
18
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
19
|
+
end
|
20
|
+
|
21
|
+
oba = ConnectionAdapters::OpenBaseAdapter.new(
|
22
|
+
OpenBase.new(database, host, username, password), logger
|
23
|
+
)
|
24
|
+
|
25
|
+
oba
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
module ConnectionAdapters
|
31
|
+
class OpenBaseColumn < Column #:nodoc:
|
32
|
+
private
|
33
|
+
def simplified_type(field_type)
|
34
|
+
return :integer if field_type.downcase =~ /long/
|
35
|
+
return :float if field_type.downcase == "money"
|
36
|
+
return :binary if field_type.downcase == "object"
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
# The OpenBase adapter works with the Ruby/Openbase driver by Tetsuya Suzuki.
|
41
|
+
# http://www.spice-of-life.net/ruby-openbase/ (needs version 0.7.3+)
|
42
|
+
#
|
43
|
+
# Options:
|
44
|
+
#
|
45
|
+
# * <tt>:host</tt> -- Defaults to localhost
|
46
|
+
# * <tt>:username</tt> -- Defaults to nothing
|
47
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
48
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
49
|
+
#
|
50
|
+
# The OpenBase adapter will make use of OpenBase's ability to generate unique ids
|
51
|
+
# for any column with an unique index applied. Thus, if the value of a primary
|
52
|
+
# key is not specified at the time an INSERT is performed, the adapter will prefetch
|
53
|
+
# a unique id for the primary key. This prefetching is also necessary in order
|
54
|
+
# to return the id after an insert.
|
55
|
+
#
|
56
|
+
# Caveat: Operations involving LIMIT and OFFSET do not yet work!
|
57
|
+
#
|
58
|
+
# Maintainer: derrickspell@cdmplus.com
|
59
|
+
class OpenBaseAdapter < AbstractAdapter
|
60
|
+
def adapter_name
|
61
|
+
'OpenBase'
|
62
|
+
end
|
63
|
+
|
64
|
+
def native_database_types
|
65
|
+
{
|
66
|
+
:primary_key => "integer UNIQUE INDEX DEFAULT _rowid",
|
67
|
+
:string => { :name => "char", :limit => 4096 },
|
68
|
+
:text => { :name => "text" },
|
69
|
+
:integer => { :name => "integer" },
|
70
|
+
:float => { :name => "float" },
|
71
|
+
:datetime => { :name => "datetime" },
|
72
|
+
:timestamp => { :name => "timestamp" },
|
73
|
+
:time => { :name => "time" },
|
74
|
+
:date => { :name => "date" },
|
75
|
+
:binary => { :name => "object" },
|
76
|
+
:boolean => { :name => "boolean" }
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def supports_migrations?
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def prefetch_primary_key?(table_name = nil)
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def default_sequence_name(table_name, primary_key) # :nodoc:
|
89
|
+
"#{table_name} #{primary_key}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_sequence_value(sequence_name)
|
93
|
+
ary = sequence_name.split(' ')
|
94
|
+
if (!ary[1]) then
|
95
|
+
ary[0] =~ /(\w+)_nonstd_seq/
|
96
|
+
ary[0] = $1
|
97
|
+
end
|
98
|
+
@connection.unique_row_id(ary[0], ary[1])
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# QUOTING ==================================================
|
103
|
+
|
104
|
+
def quote(value, column = nil)
|
105
|
+
if value.kind_of?(String) && column && column.type == :binary
|
106
|
+
"'#{@connection.insert_binary(value)}'"
|
107
|
+
else
|
108
|
+
super
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def quoted_true
|
113
|
+
"1"
|
114
|
+
end
|
115
|
+
|
116
|
+
def quoted_false
|
117
|
+
"0"
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
# DATABASE STATEMENTS ======================================
|
123
|
+
|
124
|
+
def add_limit_offset!(sql, options) #:nodoc
|
125
|
+
if limit = options[:limit]
|
126
|
+
unless offset = options[:offset]
|
127
|
+
sql << " RETURN RESULTS #{limit}"
|
128
|
+
else
|
129
|
+
limit = limit + offset
|
130
|
+
sql << " RETURN RESULTS #{offset} TO #{limit}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def select_all(sql, name = nil) #:nodoc:
|
136
|
+
select(sql, name)
|
137
|
+
end
|
138
|
+
|
139
|
+
def select_one(sql, name = nil) #:nodoc:
|
140
|
+
add_limit_offset!(sql,{:limit => 1})
|
141
|
+
results = select(sql, name)
|
142
|
+
results.first if results
|
143
|
+
end
|
144
|
+
|
145
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
146
|
+
execute(sql, name)
|
147
|
+
update_nulls_after_insert(sql, name, pk, id_value, sequence_name)
|
148
|
+
id_value
|
149
|
+
end
|
150
|
+
|
151
|
+
def execute(sql, name = nil) #:nodoc:
|
152
|
+
log(sql, name) { @connection.execute(sql) }
|
153
|
+
end
|
154
|
+
|
155
|
+
def update(sql, name = nil) #:nodoc:
|
156
|
+
execute(sql, name).rows_affected
|
157
|
+
end
|
158
|
+
|
159
|
+
alias_method :delete, :update #:nodoc:
|
160
|
+
#=begin
|
161
|
+
def begin_db_transaction #:nodoc:
|
162
|
+
execute "START TRANSACTION"
|
163
|
+
rescue Exception
|
164
|
+
# Transactions aren't supported
|
165
|
+
end
|
166
|
+
|
167
|
+
def commit_db_transaction #:nodoc:
|
168
|
+
execute "COMMIT"
|
169
|
+
rescue Exception
|
170
|
+
# Transactions aren't supported
|
171
|
+
end
|
172
|
+
|
173
|
+
def rollback_db_transaction #:nodoc:
|
174
|
+
execute "ROLLBACK"
|
175
|
+
rescue Exception
|
176
|
+
# Transactions aren't supported
|
177
|
+
end
|
178
|
+
#=end
|
179
|
+
|
180
|
+
# SCHEMA STATEMENTS ========================================
|
181
|
+
|
182
|
+
# Return the list of all tables in the schema search path.
|
183
|
+
def tables(name = nil) #:nodoc:
|
184
|
+
tables = @connection.tables
|
185
|
+
tables.reject! { |t| /\A_SYS_/ === t }
|
186
|
+
tables
|
187
|
+
end
|
188
|
+
|
189
|
+
def columns(table_name, name = nil) #:nodoc:
|
190
|
+
sql = "SELECT * FROM _sys_tables "
|
191
|
+
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
|
192
|
+
sql << "ORDER BY columnNumber"
|
193
|
+
columns = []
|
194
|
+
select_all(sql, name).each do |row|
|
195
|
+
columns << OpenBaseColumn.new(row["fieldname"],
|
196
|
+
default_value(row["defaultvalue"]),
|
197
|
+
sql_type_name(row["typename"],row["length"]),
|
198
|
+
row["notnull"]
|
199
|
+
)
|
200
|
+
# breakpoint() if row["fieldname"] == "content"
|
201
|
+
end
|
202
|
+
columns
|
203
|
+
end
|
204
|
+
|
205
|
+
def indexes(table_name, name = nil)#:nodoc:
|
206
|
+
sql = "SELECT fieldname, notnull, searchindex, uniqueindex, clusteredindex FROM _sys_tables "
|
207
|
+
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
|
208
|
+
sql << "AND primarykey=0 "
|
209
|
+
sql << "AND (searchindex=1 OR uniqueindex=1 OR clusteredindex=1) "
|
210
|
+
sql << "ORDER BY columnNumber"
|
211
|
+
indexes = []
|
212
|
+
execute(sql, name).each do |row|
|
213
|
+
indexes << IndexDefinition.new(table_name,index_name(row),row[3]==1,[row[0]])
|
214
|
+
end
|
215
|
+
indexes
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
private
|
220
|
+
def select(sql, name = nil)
|
221
|
+
sql = translate_sql(sql)
|
222
|
+
results = execute(sql, name)
|
223
|
+
|
224
|
+
date_cols = []
|
225
|
+
col_names = []
|
226
|
+
results.column_infos.each do |info|
|
227
|
+
col_names << info.name
|
228
|
+
date_cols << info.name if info.type == "date"
|
229
|
+
end
|
230
|
+
|
231
|
+
rows = []
|
232
|
+
if ( results.rows_affected )
|
233
|
+
results.each do |row| # loop through result rows
|
234
|
+
hashed_row = {}
|
235
|
+
row.each_index do |index|
|
236
|
+
hashed_row["#{col_names[index]}"] = row[index] unless col_names[index] == "_rowid"
|
237
|
+
end
|
238
|
+
date_cols.each do |name|
|
239
|
+
unless hashed_row["#{name}"].nil? or hashed_row["#{name}"].empty?
|
240
|
+
hashed_row["#{name}"] = Date.parse(hashed_row["#{name}"],false).to_s
|
241
|
+
end
|
242
|
+
end
|
243
|
+
rows << hashed_row
|
244
|
+
end
|
245
|
+
end
|
246
|
+
rows
|
247
|
+
end
|
248
|
+
|
249
|
+
def default_value(value)
|
250
|
+
# Boolean type values
|
251
|
+
return true if value =~ /true/
|
252
|
+
return false if value =~ /false/
|
253
|
+
|
254
|
+
# Date / Time magic values
|
255
|
+
return Time.now.to_s if value =~ /^now\(\)/i
|
256
|
+
|
257
|
+
# Empty strings should be set to null
|
258
|
+
return nil if value.empty?
|
259
|
+
|
260
|
+
# Otherwise return what we got from OpenBase
|
261
|
+
# and hope for the best...
|
262
|
+
return value
|
263
|
+
end
|
264
|
+
|
265
|
+
def sql_type_name(type_name, length)
|
266
|
+
return "#{type_name}(#{length})" if ( type_name =~ /char/ )
|
267
|
+
type_name
|
268
|
+
end
|
269
|
+
|
270
|
+
def index_name(row = [])
|
271
|
+
name = ""
|
272
|
+
name << "UNIQUE " if row[3]
|
273
|
+
name << "CLUSTERED " if row[4]
|
274
|
+
name << "INDEX"
|
275
|
+
name
|
276
|
+
end
|
277
|
+
|
278
|
+
def translate_sql(sql)
|
279
|
+
|
280
|
+
# Change table.* to list of columns in table
|
281
|
+
while (sql =~ /SELECT.*\s(\w+)\.\*/)
|
282
|
+
table = $1
|
283
|
+
cols = columns(table)
|
284
|
+
if ( cols.size == 0 ) then
|
285
|
+
# Maybe this is a table alias
|
286
|
+
sql =~ /FROM(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
|
287
|
+
$1 =~ /[\s|,](\w+)\s+#{table}[\s|,]/ # get the tablename for this alias
|
288
|
+
cols = columns($1)
|
289
|
+
end
|
290
|
+
select_columns = []
|
291
|
+
cols.each do |col|
|
292
|
+
select_columns << table + '.' + col.name
|
293
|
+
end
|
294
|
+
sql.gsub!(table + '.*',select_columns.join(", ")) if select_columns
|
295
|
+
end
|
296
|
+
|
297
|
+
# Change JOIN clause to table list and WHERE condition
|
298
|
+
while (sql =~ /JOIN/)
|
299
|
+
sql =~ /((LEFT )?(OUTER )?JOIN (\w+) ON )(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
|
300
|
+
join_clause = $1 + $5
|
301
|
+
is_outer_join = $3
|
302
|
+
join_table = $4
|
303
|
+
join_condition = $5
|
304
|
+
join_condition.gsub!(/=/,"*") if is_outer_join
|
305
|
+
if (sql =~ /WHERE/)
|
306
|
+
sql.gsub!(/WHERE/,"WHERE (#{join_condition}) AND")
|
307
|
+
else
|
308
|
+
sql.gsub!(join_clause,"#{join_clause} WHERE #{join_condition}")
|
309
|
+
end
|
310
|
+
sql =~ /(FROM .+?)(?:LEFT|OUTER|JOIN|WHERE|$)/
|
311
|
+
from_clause = $1
|
312
|
+
sql.gsub!(from_clause,"#{from_clause}, #{join_table} ")
|
313
|
+
sql.gsub!(join_clause,"")
|
314
|
+
end
|
315
|
+
|
316
|
+
# ORDER BY _rowid if no explicit ORDER BY
|
317
|
+
# This will ensure that find(:first) returns the first inserted row
|
318
|
+
if (sql !~ /(ORDER BY)|(GROUP BY)/)
|
319
|
+
if (sql =~ /RETURN RESULTS/)
|
320
|
+
sql.sub!(/RETURN RESULTS/,"ORDER BY _rowid RETURN RESULTS")
|
321
|
+
else
|
322
|
+
sql << " ORDER BY _rowid"
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
sql
|
327
|
+
end
|
328
|
+
|
329
|
+
def update_nulls_after_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
330
|
+
sql =~ /INSERT INTO (\w+) \((.*)\) VALUES\s*\((.*)\)/m
|
331
|
+
table = $1
|
332
|
+
cols = $2
|
333
|
+
values = $3
|
334
|
+
cols = cols.split(',')
|
335
|
+
values.gsub!(/'[^']*'/,"''")
|
336
|
+
values.gsub!(/"[^"]*"/,"\"\"")
|
337
|
+
values = values.split(',')
|
338
|
+
update_cols = []
|
339
|
+
values.each_index { |index| update_cols << cols[index] if values[index] =~ /\s*NULL\s*/ }
|
340
|
+
update_sql = "UPDATE #{table} SET"
|
341
|
+
update_cols.each { |col| update_sql << " #{col}=NULL," unless col.empty? }
|
342
|
+
update_sql.chop!()
|
343
|
+
update_sql << " WHERE #{pk}=#{quote(id_value)}"
|
344
|
+
execute(update_sql, name + " NULL Correction") if update_cols.size > 0
|
345
|
+
end
|
346
|
+
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# oracle_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g
|
2
2
|
#
|
3
3
|
# Original author: Graham Jenkins
|
4
4
|
#
|
@@ -30,18 +30,28 @@ begin
|
|
30
30
|
|
31
31
|
module ActiveRecord
|
32
32
|
class Base
|
33
|
-
def self.
|
33
|
+
def self.oracle_connection(config) #:nodoc:
|
34
34
|
# Use OCI8AutoRecover instead of normal OCI8 driver.
|
35
|
-
ConnectionAdapters::
|
35
|
+
ConnectionAdapters::OracleAdapter.new OCI8AutoRecover.new(config), logger
|
36
|
+
end
|
37
|
+
|
38
|
+
# for backwards-compatibility
|
39
|
+
def self.oci_connection(config) #:nodoc:
|
40
|
+
config[:database] = config[:host]
|
41
|
+
self.oracle_connection(config)
|
36
42
|
end
|
37
43
|
|
38
44
|
# Enable the id column to be bound into the sql later, by the adapter's insert method.
|
39
45
|
# This is preferable to inserting the hard-coded value here, because the insert method
|
40
46
|
# needs to know the id value explicitly.
|
41
|
-
|
47
|
+
def attributes_with_quotes_pre_oracle #:nodoc:
|
48
|
+
attributes_with_quotes
|
49
|
+
end
|
50
|
+
|
51
|
+
|
42
52
|
def attributes_with_quotes(creating = true) #:nodoc:
|
43
|
-
aq =
|
44
|
-
if connection.class == ConnectionAdapters::
|
53
|
+
aq = attributes_with_quotes_pre_oracle creating
|
54
|
+
if connection.class == ConnectionAdapters::OracleAdapter
|
45
55
|
aq[self.class.primary_key] = ":id" if creating && aq[self.class.primary_key].nil?
|
46
56
|
end
|
47
57
|
aq
|
@@ -51,12 +61,12 @@ begin
|
|
51
61
|
# and write back the data.
|
52
62
|
after_save :write_lobs
|
53
63
|
def write_lobs() #:nodoc:
|
54
|
-
if connection.is_a?(ConnectionAdapters::
|
64
|
+
if connection.is_a?(ConnectionAdapters::OracleAdapter)
|
55
65
|
self.class.columns.select { |c| c.type == :binary }.each { |c|
|
56
66
|
value = self[c.name]
|
57
67
|
next if value.nil? || (value == '')
|
58
68
|
lob = connection.select_one(
|
59
|
-
"
|
69
|
+
"SELECT #{ c.name} FROM #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}",
|
60
70
|
'Writable Large Object')[c.name]
|
61
71
|
lob.write value
|
62
72
|
}
|
@@ -68,7 +78,7 @@ begin
|
|
68
78
|
|
69
79
|
|
70
80
|
module ConnectionAdapters #:nodoc:
|
71
|
-
class
|
81
|
+
class OracleColumn < Column #:nodoc:
|
72
82
|
attr_reader :sql_type
|
73
83
|
|
74
84
|
# overridden to add the concept of scale, required to differentiate
|
@@ -102,7 +112,8 @@ begin
|
|
102
112
|
when /char/i : :string
|
103
113
|
when /num|float|double|dec|real|int/i : @scale == 0 ? :integer : :float
|
104
114
|
when /date|time/i : @name =~ /_at$/ ? :time : :datetime
|
105
|
-
when /
|
115
|
+
when /clob/i : :text
|
116
|
+
when /blob/i : :binary
|
106
117
|
end
|
107
118
|
end
|
108
119
|
|
@@ -149,16 +160,19 @@ begin
|
|
149
160
|
# * Default values that are functions (such as "SYSDATE") are not
|
150
161
|
# supported. This is a restriction of the way ActiveRecord supports
|
151
162
|
# default values.
|
163
|
+
# * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which
|
164
|
+
# is supported in Oracle9i and later. You will need to use #finder_sql for
|
165
|
+
# has_and_belongs_to_many associations to run against Oracle8.
|
152
166
|
#
|
153
|
-
#
|
167
|
+
# Required parameters:
|
154
168
|
#
|
155
|
-
# * <tt>:username</tt>
|
156
|
-
# * <tt>:password</tt>
|
157
|
-
# * <tt>:
|
158
|
-
class
|
169
|
+
# * <tt>:username</tt>
|
170
|
+
# * <tt>:password</tt>
|
171
|
+
# * <tt>:database</tt>
|
172
|
+
class OracleAdapter < AbstractAdapter
|
159
173
|
|
160
174
|
def adapter_name #:nodoc:
|
161
|
-
'
|
175
|
+
'Oracle'
|
162
176
|
end
|
163
177
|
|
164
178
|
def supports_migrations? #:nodoc:
|
@@ -167,9 +181,9 @@ begin
|
|
167
181
|
|
168
182
|
def native_database_types #:nodoc
|
169
183
|
{
|
170
|
-
:primary_key => "NUMBER(38) NOT NULL",
|
184
|
+
:primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
|
171
185
|
:string => { :name => "VARCHAR2", :limit => 255 },
|
172
|
-
:text => { :name => "
|
186
|
+
:text => { :name => "CLOB" },
|
173
187
|
:integer => { :name => "NUMBER", :limit => 38 },
|
174
188
|
:float => { :name => "NUMBER" },
|
175
189
|
:datetime => { :name => "DATE" },
|
@@ -181,6 +195,9 @@ begin
|
|
181
195
|
}
|
182
196
|
end
|
183
197
|
|
198
|
+
def table_alias_length
|
199
|
+
30
|
200
|
+
end
|
184
201
|
|
185
202
|
# QUOTING ==================================================
|
186
203
|
#
|
@@ -197,7 +214,7 @@ begin
|
|
197
214
|
end
|
198
215
|
|
199
216
|
def quote(value, column = nil) #:nodoc:
|
200
|
-
if column
|
217
|
+
if column && column.type == :binary then %Q{empty_#{ column.sql_type }()}
|
201
218
|
else case value
|
202
219
|
when String then %Q{'#{quote_string(value)}'}
|
203
220
|
when NilClass then 'null'
|
@@ -211,7 +228,8 @@ begin
|
|
211
228
|
end
|
212
229
|
|
213
230
|
|
214
|
-
# CONNECTION MANAGEMENT
|
231
|
+
# CONNECTION MANAGEMENT ====================================
|
232
|
+
#
|
215
233
|
|
216
234
|
# Returns true if the connection is active.
|
217
235
|
def active?
|
@@ -220,17 +238,23 @@ begin
|
|
220
238
|
# last known state, which isn't good enough if the connection has
|
221
239
|
# gone stale since the last use.
|
222
240
|
@connection.ping
|
223
|
-
rescue
|
241
|
+
rescue OCIException
|
224
242
|
false
|
225
243
|
end
|
226
244
|
|
227
245
|
# Reconnects to the database.
|
228
246
|
def reconnect!
|
229
247
|
@connection.reset!
|
230
|
-
rescue
|
248
|
+
rescue OCIException => e
|
231
249
|
@logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}"
|
232
250
|
end
|
233
251
|
|
252
|
+
# Disconnects from the database.
|
253
|
+
def disconnect!
|
254
|
+
@connection.logoff rescue nil
|
255
|
+
@connection.active = false
|
256
|
+
end
|
257
|
+
|
234
258
|
|
235
259
|
# DATABASE STATEMENTS ======================================
|
236
260
|
#
|
@@ -300,6 +324,10 @@ begin
|
|
300
324
|
#
|
301
325
|
# see: abstract/schema_statements.rb
|
302
326
|
|
327
|
+
def current_database #:nodoc:
|
328
|
+
select_one("select sys_context('userenv','db_name') db from dual")["db"]
|
329
|
+
end
|
330
|
+
|
303
331
|
def tables(name = nil) #:nodoc:
|
304
332
|
select_all("select lower(table_name) from user_tables").inject([]) do | tabs, t |
|
305
333
|
tabs << t.to_a.first.last
|
@@ -332,10 +360,7 @@ begin
|
|
332
360
|
end
|
333
361
|
|
334
362
|
def columns(table_name, name = nil) #:nodoc:
|
335
|
-
|
336
|
-
owner = table_name.include?('.') ? "'#{table_name.split('.').first}'" : "user"
|
337
|
-
table = "'#{table_name.split('.').last}'"
|
338
|
-
scope = (owner == "user" ? "user" : "all")
|
363
|
+
table_info = @connection.object_info(table_name)
|
339
364
|
|
340
365
|
table_cols = %Q{
|
341
366
|
select column_name, data_type, data_default, nullable,
|
@@ -343,26 +368,20 @@ begin
|
|
343
368
|
'VARCHAR2', data_length,
|
344
369
|
null) as length,
|
345
370
|
decode(data_type, 'NUMBER', data_scale, null) as scale
|
346
|
-
from
|
347
|
-
where
|
348
|
-
and
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
if scope == "all"
|
353
|
-
table_cols << %Q{
|
354
|
-
and cat.owner = #{owner}
|
355
|
-
and syn.owner (+)= cat.owner }
|
356
|
-
end
|
371
|
+
from all_tab_columns
|
372
|
+
where owner = '#{table_info.schema}'
|
373
|
+
and table_name = '#{table_info.name}'
|
374
|
+
order by column_id
|
375
|
+
}
|
357
376
|
|
358
377
|
select_all(table_cols, name).map do |row|
|
359
378
|
row['data_default'].sub!(/^'(.*)'\s*$/, '\1') if row['data_default']
|
360
|
-
|
361
|
-
|
379
|
+
OracleColumn.new(
|
380
|
+
oracle_downcase(row['column_name']),
|
362
381
|
row['data_default'],
|
363
|
-
row['data_type'],
|
364
|
-
row['length'],
|
365
|
-
row['scale'],
|
382
|
+
row['data_type'],
|
383
|
+
(l = row['length']).nil? ? nil : l.to_i,
|
384
|
+
(s = row['scale']).nil? ? nil : s.to_i,
|
366
385
|
row['nullable'] == 'Y'
|
367
386
|
)
|
368
387
|
end
|
@@ -370,17 +389,17 @@ begin
|
|
370
389
|
|
371
390
|
def create_table(name, options = {}) #:nodoc:
|
372
391
|
super(name, options)
|
373
|
-
execute "CREATE SEQUENCE #{name}_seq"
|
392
|
+
execute "CREATE SEQUENCE #{name}_seq" unless options[:id] == false
|
374
393
|
end
|
375
394
|
|
376
395
|
def rename_table(name, new_name) #:nodoc:
|
377
396
|
execute "RENAME #{name} TO #{new_name}"
|
378
|
-
execute "RENAME #{name}_seq TO #{new_name}_seq"
|
397
|
+
execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil
|
379
398
|
end
|
380
399
|
|
381
400
|
def drop_table(name) #:nodoc:
|
382
401
|
super(name)
|
383
|
-
execute "DROP SEQUENCE #{name}_seq"
|
402
|
+
execute "DROP SEQUENCE #{name}_seq" rescue nil
|
384
403
|
end
|
385
404
|
|
386
405
|
def remove_index(table_name, options = {}) #:nodoc:
|
@@ -451,7 +470,7 @@ begin
|
|
451
470
|
|
452
471
|
def select(sql, name = nil)
|
453
472
|
cursor = log(sql, name) { @connection.exec sql }
|
454
|
-
cols = cursor.get_col_names.map { |x|
|
473
|
+
cols = cursor.get_col_names.map { |x| oracle_downcase(x) }
|
455
474
|
rows = []
|
456
475
|
|
457
476
|
while row = cursor.fetch
|
@@ -483,7 +502,7 @@ begin
|
|
483
502
|
# I don't know anybody who does this, but we'll handle the theoretical case of a
|
484
503
|
# camelCase column name. I imagine other dbs handle this different, since there's a
|
485
504
|
# unit test that's currently failing test_oci.
|
486
|
-
def
|
505
|
+
def oracle_downcase(column_name)
|
487
506
|
column_name =~ /[a-z]/ ? column_name : column_name.downcase
|
488
507
|
end
|
489
508
|
|
@@ -492,27 +511,62 @@ begin
|
|
492
511
|
end
|
493
512
|
|
494
513
|
|
495
|
-
# This OCI8 patch may not longer be required with the upcoming
|
496
|
-
# release of version 0.2.
|
497
514
|
class OCI8 #:nodoc:
|
515
|
+
|
516
|
+
# This OCI8 patch may not longer be required with the upcoming
|
517
|
+
# release of version 0.2.
|
498
518
|
class Cursor #:nodoc:
|
499
519
|
alias :define_a_column_pre_ar :define_a_column
|
500
520
|
def define_a_column(i)
|
501
521
|
case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
|
502
522
|
when 8 : @stmt.defineByPos(i, String, 65535) # Read LONG values
|
503
523
|
when 187 : @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
|
524
|
+
when 108
|
525
|
+
if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
|
526
|
+
@stmt.defineByPos(i, String, 65535)
|
527
|
+
else
|
528
|
+
raise 'unsupported datatype'
|
529
|
+
end
|
504
530
|
else define_a_column_pre_ar i
|
505
531
|
end
|
506
532
|
end
|
507
533
|
end
|
534
|
+
|
535
|
+
# missing constant from oci8 < 0.1.14
|
536
|
+
OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
|
537
|
+
|
538
|
+
def object_info(name)
|
539
|
+
OraObject.new describe(name.to_s, OCI_PTYPE_UNK)
|
540
|
+
end
|
541
|
+
|
542
|
+
def describe(name, type)
|
543
|
+
@desc ||= @@env.alloc(OCIDescribe)
|
544
|
+
@desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
|
545
|
+
@desc.describeAny(@svc, name, type)
|
546
|
+
@desc.attrGet(OCI_ATTR_PARAM)
|
547
|
+
end
|
548
|
+
|
549
|
+
class OraObject #:nodoc:
|
550
|
+
attr_reader :schema, :name
|
551
|
+
def initialize(info)
|
552
|
+
case info.attrGet(OCI_ATTR_PTYPE)
|
553
|
+
when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
|
554
|
+
@schema = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
|
555
|
+
@name = info.attrGet(OCI_ATTR_OBJ_NAME)
|
556
|
+
when OCI_PTYPE_SYN
|
557
|
+
@schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
|
558
|
+
@name = info.attrGet(OCI_ATTR_NAME)
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
508
562
|
end
|
509
563
|
|
510
564
|
|
511
|
-
# The
|
512
|
-
# configure an OCI connection.
|
513
|
-
class
|
514
|
-
def new_connection(username, password,
|
515
|
-
conn = OCI8.new username, password,
|
565
|
+
# The OracleConnectionFactory factors out the code necessary to connect and
|
566
|
+
# configure an Oracle/OCI connection.
|
567
|
+
class OracleConnectionFactory #:nodoc:
|
568
|
+
def new_connection(username, password, database)
|
569
|
+
conn = OCI8.new username, password, database
|
516
570
|
conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
|
517
571
|
conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
|
518
572
|
conn.autocommit = true
|
@@ -538,11 +592,11 @@ begin
|
|
538
592
|
end
|
539
593
|
@@auto_retry = false
|
540
594
|
|
541
|
-
def initialize(config, factory =
|
595
|
+
def initialize(config, factory = OracleConnectionFactory.new)
|
542
596
|
@active = true
|
543
|
-
@username, @password, @
|
597
|
+
@username, @password, @database = config[:username], config[:password], config[:database]
|
544
598
|
@factory = factory
|
545
|
-
@connection = @factory.new_connection @username, @password, @
|
599
|
+
@connection = @factory.new_connection @username, @password, @database
|
546
600
|
super @connection
|
547
601
|
end
|
548
602
|
|
@@ -561,7 +615,7 @@ begin
|
|
561
615
|
def reset!
|
562
616
|
logoff rescue nil
|
563
617
|
begin
|
564
|
-
@connection = @factory.new_connection @username, @password, @
|
618
|
+
@connection = @factory.new_connection @username, @password, @database
|
565
619
|
__setobj__ @connection
|
566
620
|
@active = true
|
567
621
|
rescue
|
@@ -584,7 +638,7 @@ begin
|
|
584
638
|
|
585
639
|
begin
|
586
640
|
@connection.exec(sql, *bindvars)
|
587
|
-
rescue
|
641
|
+
rescue OCIException => e
|
588
642
|
raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
|
589
643
|
@active = false
|
590
644
|
raise unless should_retry
|
@@ -598,4 +652,16 @@ begin
|
|
598
652
|
|
599
653
|
rescue LoadError
|
600
654
|
# OCI8 driver is unavailable.
|
655
|
+
module ActiveRecord # :nodoc:
|
656
|
+
class Base
|
657
|
+
def self.oracle_connection(config) # :nodoc:
|
658
|
+
# Set up a reasonable error message
|
659
|
+
raise LoadError, "Oracle/OCI libraries could not be loaded."
|
660
|
+
end
|
661
|
+
def self.oci_connection(config) # :nodoc:
|
662
|
+
# Set up a reasonable error message
|
663
|
+
raise LoadError, "Oracle/OCI libraries could not be loaded."
|
664
|
+
end
|
665
|
+
end
|
666
|
+
end
|
601
667
|
end
|