activerecord 1.6.0 → 1.7.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 +78 -0
- data/README +20 -29
- data/RUNNING_UNIT_TESTS +1 -2
- data/examples/validation.rb +0 -3
- data/install.rb +3 -16
- data/lib/active_record.rb +11 -4
- data/lib/active_record/aggregations.rb +2 -2
- data/lib/active_record/associations.rb +8 -8
- data/lib/active_record/associations/association_collection.rb +1 -1
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -1
- data/lib/active_record/base.rb +117 -43
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/connection_adapters/abstract_adapter.rb +7 -14
- data/lib/active_record/connection_adapters/db2_adapter.rb +33 -22
- data/lib/active_record/connection_adapters/mysql_adapter.rb +74 -33
- data/lib/active_record/connection_adapters/oci_adapter.rb +265 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -3
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +13 -4
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +158 -67
- data/lib/active_record/deprecated_associations.rb +4 -4
- data/lib/active_record/fixtures.rb +12 -5
- data/lib/active_record/locking.rb +22 -22
- data/lib/active_record/observer.rb +6 -3
- data/lib/active_record/timestamp.rb +15 -5
- data/lib/active_record/transactions.rb +4 -4
- data/lib/active_record/validations.rb +272 -189
- data/lib/active_record/wrappings.rb +2 -2
- data/rakefile +17 -2
- data/test/aaa_create_tables_test.rb +58 -0
- data/test/abstract_unit.rb +3 -2
- data/test/aggregations_test.rb +0 -1
- data/test/associations_test.rb +27 -28
- data/test/base_test.rb +74 -2
- data/test/binary_test.rb +6 -2
- data/test/class_inheritable_attributes_test.rb +1 -1
- data/test/column_alias_test.rb +9 -2
- data/test/connections/native_oci/connection.rb +25 -0
- data/test/connections/native_sqlite/connection.rb +4 -1
- data/test/connections/native_sqlite3/connection.rb +4 -2
- data/test/deprecated_associations_test.rb +4 -5
- data/test/finder_test.rb +20 -4
- data/test/fixtures/db_definitions/create_oracle_db.bat +5 -0
- data/test/fixtures/db_definitions/create_oracle_db.sh +5 -0
- data/test/fixtures/db_definitions/db2.drop.sql +18 -0
- data/test/fixtures/db_definitions/db2.sql +1 -0
- data/test/fixtures/db_definitions/db22.drop.sql +2 -0
- data/test/fixtures/db_definitions/db22.sql +1 -0
- data/test/fixtures/db_definitions/drop_oracle_tables.sql +35 -0
- data/test/fixtures/db_definitions/drop_oracle_tables2.sql +3 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +18 -0
- data/test/fixtures/db_definitions/mysql.sql +2 -1
- data/test/fixtures/db_definitions/mysql2.drop.sql +2 -0
- data/test/fixtures/db_definitions/mysql2.sql +1 -0
- data/test/fixtures/db_definitions/oci.drop.sql +18 -0
- data/test/fixtures/db_definitions/oci.sql +167 -0
- data/test/fixtures/db_definitions/oci2.drop.sql +2 -0
- data/test/fixtures/db_definitions/oci2.sql +6 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +18 -0
- data/test/fixtures/db_definitions/postgresql.sql +2 -1
- data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
- data/test/fixtures/db_definitions/postgresql2.sql +2 -1
- data/test/fixtures/db_definitions/sqlite.drop.sql +18 -0
- data/test/fixtures/db_definitions/sqlite.sql +2 -1
- data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite2.sql +1 -0
- data/test/fixtures/db_definitions/sqlserver.drop.sql +18 -0
- data/test/fixtures/db_definitions/sqlserver.sql +1 -0
- data/test/fixtures/db_definitions/sqlserver2.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlserver2.sql +1 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/topics.yml +3 -3
- data/test/lifecycle_test.rb +0 -1
- data/test/modules_test.rb +0 -1
- data/test/reflection_test.rb +0 -1
- data/test/validations_test.rb +229 -41
- metadata +36 -28
- data/dev-utils/eval_debugger.rb +0 -14
- data/lib/active_record/support/binding_of_caller.rb +0 -83
- data/lib/active_record/support/breakpoint.rb +0 -518
- data/lib/active_record/support/class_attribute_accessors.rb +0 -57
- data/lib/active_record/support/class_inheritable_attributes.rb +0 -117
- data/lib/active_record/support/clean_logger.rb +0 -10
- data/lib/active_record/support/core_ext.rb +0 -1
- data/lib/active_record/support/core_ext/hash.rb +0 -5
- data/lib/active_record/support/core_ext/hash/keys.rb +0 -35
- data/lib/active_record/support/core_ext/numeric.rb +0 -7
- data/lib/active_record/support/core_ext/numeric/bytes.rb +0 -33
- data/lib/active_record/support/core_ext/numeric/time.rb +0 -59
- data/lib/active_record/support/core_ext/object_and_class.rb +0 -24
- data/lib/active_record/support/core_ext/string.rb +0 -5
- data/lib/active_record/support/core_ext/string/inflections.rb +0 -45
- data/lib/active_record/support/dependencies.rb +0 -63
- data/lib/active_record/support/inflector.rb +0 -84
- data/lib/active_record/support/misc.rb +0 -8
- data/lib/active_record/support/module_attribute_accessors.rb +0 -57
@@ -22,7 +22,7 @@ module ActiveRecord
|
|
22
22
|
# * (9) after_save
|
23
23
|
#
|
24
24
|
# That's a total of nine callbacks, which gives you immense power to react and prepare for each state in the
|
25
|
-
# Active Record
|
25
|
+
# Active Record lifecycle.
|
26
26
|
#
|
27
27
|
# Examples:
|
28
28
|
# class CreditCard < ActiveRecord::Base
|
@@ -125,7 +125,7 @@ module ActiveRecord
|
|
125
125
|
# end
|
126
126
|
#
|
127
127
|
# def decrypt(value)
|
128
|
-
# # Secrecy is
|
128
|
+
# # Secrecy is unveiled
|
129
129
|
# end
|
130
130
|
# end
|
131
131
|
#
|
@@ -63,18 +63,6 @@ module ActiveRecord
|
|
63
63
|
#
|
64
64
|
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
|
65
65
|
# may be returned on an error.
|
66
|
-
#
|
67
|
-
# == Connecting to another database for a single model
|
68
|
-
#
|
69
|
-
# To support different connections for different classes, you can
|
70
|
-
# simply call establish_connection with the classes you wish to have
|
71
|
-
# different connections for:
|
72
|
-
#
|
73
|
-
# class Courses < ActiveRecord::Base
|
74
|
-
# ...
|
75
|
-
# end
|
76
|
-
#
|
77
|
-
# Courses.establish_connection( ... )
|
78
66
|
def self.establish_connection(spec = nil)
|
79
67
|
case spec
|
80
68
|
when nil
|
@@ -151,7 +139,7 @@ module ActiveRecord
|
|
151
139
|
end
|
152
140
|
|
153
141
|
# Converts all strings in a hash to symbols.
|
154
|
-
def self.symbolize_strings_in_hash(hash)
|
142
|
+
def self.symbolize_strings_in_hash(hash) #:nodoc:
|
155
143
|
hash.symbolize_keys
|
156
144
|
end
|
157
145
|
end
|
@@ -326,7 +314,7 @@ module ActiveRecord
|
|
326
314
|
# Commits the transaction (and turns on auto-committing).
|
327
315
|
def commit_db_transaction() end
|
328
316
|
|
329
|
-
#
|
317
|
+
# Rolls back the transaction (and turns on auto-committing). Must be done if the transaction block
|
330
318
|
# raises an exception or returns false.
|
331
319
|
def rollback_db_transaction() end
|
332
320
|
|
@@ -356,6 +344,11 @@ module ActiveRecord
|
|
356
344
|
name
|
357
345
|
end
|
358
346
|
|
347
|
+
# Returns the human-readable name of the adapter. Use mixed case - one can always use downcase if needed.
|
348
|
+
def adapter_name()
|
349
|
+
'Abstract'
|
350
|
+
end
|
351
|
+
|
359
352
|
# Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
|
360
353
|
def structure_dump() end
|
361
354
|
|
@@ -1,5 +1,4 @@
|
|
1
|
-
#
|
2
|
-
# author: Maik Schmidt <contact@maik-schmidt.de>
|
1
|
+
# Author: Maik Schmidt <contact@maik-schmidt.de>
|
3
2
|
|
4
3
|
require 'active_record/connection_adapters/abstract_adapter'
|
5
4
|
|
@@ -29,7 +28,14 @@ begin
|
|
29
28
|
end
|
30
29
|
|
31
30
|
module ConnectionAdapters
|
32
|
-
|
31
|
+
# The DB2 adapter works with the C-based CLI driver (http://raa.ruby-lang.org/project/ruby-db2/).
|
32
|
+
#
|
33
|
+
# Options:
|
34
|
+
#
|
35
|
+
# * <tt>:username</tt> -- Defaults to nothing
|
36
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
37
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
38
|
+
class DB2Adapter < AbstractAdapter
|
33
39
|
def select_all(sql, name = nil)
|
34
40
|
select(sql, name)
|
35
41
|
end
|
@@ -75,6 +81,10 @@ begin
|
|
75
81
|
|
76
82
|
def quote_column_name(name) name; end
|
77
83
|
|
84
|
+
def adapter_name()
|
85
|
+
'DB2'
|
86
|
+
end
|
87
|
+
|
78
88
|
def quote_string(s)
|
79
89
|
s.gsub(/'/, "''") # ' (for ruby-mode)
|
80
90
|
end
|
@@ -100,30 +110,31 @@ begin
|
|
100
110
|
end
|
101
111
|
|
102
112
|
private
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
113
|
+
|
114
|
+
def last_insert_id
|
115
|
+
row = select_one(<<-GETID.strip)
|
116
|
+
with temp(id) as (values (identity_val_local())) select * from temp
|
117
|
+
GETID
|
118
|
+
row['id'].to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
def select(sql, name = nil)
|
122
|
+
stmt = nil
|
123
|
+
log(sql, name, @connection) do |connection|
|
124
|
+
stmt = DB2::Statement.new(connection)
|
125
|
+
stmt.exec_direct(sql + " with ur")
|
108
126
|
end
|
109
127
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
stmt = DB2::Statement.new(connection)
|
114
|
-
stmt.exec_direct(sql + " with ur")
|
115
|
-
end
|
116
|
-
|
117
|
-
rows = []
|
118
|
-
while row = stmt.fetch_as_hash
|
119
|
-
rows << row
|
120
|
-
end
|
121
|
-
stmt.free
|
122
|
-
rows
|
128
|
+
rows = []
|
129
|
+
while row = stmt.fetch_as_hash
|
130
|
+
rows << row
|
123
131
|
end
|
132
|
+
stmt.free
|
133
|
+
rows
|
134
|
+
end
|
124
135
|
end
|
125
136
|
end
|
126
137
|
end
|
127
138
|
rescue LoadError
|
128
139
|
# DB2 driver is unavailable.
|
129
|
-
end
|
140
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'active_record/connection_adapters/abstract_adapter'
|
2
2
|
require 'parsedate'
|
3
|
-
|
3
|
+
|
4
4
|
module ActiveRecord
|
5
5
|
class Base
|
6
|
-
# Establishes a connection to the database that's used by all Active Record objects
|
6
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
7
7
|
def self.mysql_connection(config) # :nodoc:
|
8
8
|
unless self.class.const_defined?(:Mysql)
|
9
9
|
begin
|
@@ -19,62 +19,98 @@ module ActiveRecord
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
22
|
+
|
22
23
|
symbolize_strings_in_hash(config)
|
24
|
+
|
23
25
|
host = config[:host]
|
24
26
|
port = config[:port]
|
25
27
|
socket = config[:socket]
|
26
28
|
username = config[:username] ? config[:username].to_s : 'root'
|
27
29
|
password = config[:password].to_s
|
28
|
-
|
30
|
+
|
29
31
|
if config.has_key?(:database)
|
30
32
|
database = config[:database]
|
31
33
|
else
|
32
34
|
raise ArgumentError, "No database specified. Missing argument: database."
|
33
35
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
)
|
36
|
+
|
37
|
+
mysql = Mysql.init
|
38
|
+
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
|
39
|
+
ConnectionAdapters::MysqlAdapter.new(mysql.real_connect(host, username, password, database, port, socket), logger, [host, username, password, database, port, socket])
|
38
40
|
end
|
39
41
|
end
|
40
|
-
|
42
|
+
|
41
43
|
module ConnectionAdapters
|
42
|
-
|
44
|
+
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
|
45
|
+
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
|
46
|
+
#
|
47
|
+
# Options:
|
48
|
+
#
|
49
|
+
# * <tt>:host</tt> -- Defaults to localhost
|
50
|
+
# * <tt>:port</tt> -- Defaults to 3306
|
51
|
+
# * <tt>:socket</tt> -- Defaults to /tmp/mysql.sock
|
52
|
+
# * <tt>:username</tt> -- Defaults to root
|
53
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
54
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
55
|
+
# * <tt>:sslkey</tt> -- Necessary to use MySQL with an SSL connection
|
56
|
+
# * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
|
57
|
+
# * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
|
58
|
+
# * <tt>:sslcipher</tt> -- Necessary to use MySQL with an SSL connection
|
59
|
+
class MysqlAdapter < AbstractAdapter
|
60
|
+
LOST_CONNECTION_ERROR_MESSAGES = [
|
61
|
+
"Server shutdown in progress",
|
62
|
+
"Broken pipe",
|
63
|
+
"Lost connection to MySQL server during query",
|
64
|
+
"MySQL server has gone away"
|
65
|
+
]
|
66
|
+
|
67
|
+
def initialize(connection, logger, connection_options=nil)
|
68
|
+
super(connection, logger)
|
69
|
+
@connection_options = connection_options
|
70
|
+
end
|
71
|
+
|
43
72
|
def select_all(sql, name = nil)
|
44
73
|
select(sql, name)
|
45
74
|
end
|
46
|
-
|
75
|
+
|
47
76
|
def select_one(sql, name = nil)
|
48
77
|
result = select(sql, name)
|
49
78
|
result.nil? ? nil : result.first
|
50
79
|
end
|
51
|
-
|
80
|
+
|
52
81
|
def columns(table_name, name = nil)
|
53
|
-
sql = "SHOW FIELDS FROM #{table_name}"
|
54
|
-
result = nil
|
55
|
-
log(sql, name, @connection) { |connection| result = connection.query(sql) }
|
56
|
-
|
82
|
+
sql = "SHOW FIELDS FROM #{table_name}"
|
57
83
|
columns = []
|
58
|
-
|
84
|
+
execute(sql, name).each { |field| columns << Column.new(field[0], field[4], field[1]) }
|
59
85
|
columns
|
60
86
|
end
|
61
|
-
|
87
|
+
|
62
88
|
def insert(sql, name = nil, pk = nil, id_value = nil)
|
63
89
|
execute(sql, name = nil)
|
64
90
|
return id_value || @connection.insert_id
|
65
91
|
end
|
66
|
-
|
92
|
+
|
67
93
|
def execute(sql, name = nil)
|
68
|
-
|
94
|
+
begin
|
95
|
+
return log(sql, name, @connection) { |connection| connection.query(sql) }
|
96
|
+
rescue ActiveRecord::StatementInvalid => exception
|
97
|
+
if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message.split(":").first =~ /^#{msg}/ }
|
98
|
+
@connection.real_connect(*@connection_options)
|
99
|
+
@logger.info("Retrying invalid statement with reopened connection") if @logger
|
100
|
+
return log(sql, name, @connection) { |connection| connection.query(sql) }
|
101
|
+
else
|
102
|
+
raise
|
103
|
+
end
|
104
|
+
end
|
69
105
|
end
|
70
|
-
|
106
|
+
|
71
107
|
def update(sql, name = nil)
|
72
108
|
execute(sql, name)
|
73
109
|
@connection.affected_rows
|
74
110
|
end
|
75
|
-
|
111
|
+
|
76
112
|
alias_method :delete, :update
|
77
|
-
|
113
|
+
|
78
114
|
def begin_db_transaction
|
79
115
|
begin
|
80
116
|
execute "BEGIN"
|
@@ -82,7 +118,7 @@ module ActiveRecord
|
|
82
118
|
# Transactions aren't supported
|
83
119
|
end
|
84
120
|
end
|
85
|
-
|
121
|
+
|
86
122
|
def commit_db_transaction
|
87
123
|
begin
|
88
124
|
execute "COMMIT"
|
@@ -90,7 +126,7 @@ module ActiveRecord
|
|
90
126
|
# Transactions aren't supported
|
91
127
|
end
|
92
128
|
end
|
93
|
-
|
129
|
+
|
94
130
|
def rollback_db_transaction
|
95
131
|
begin
|
96
132
|
execute "ROLLBACK"
|
@@ -98,38 +134,43 @@ module ActiveRecord
|
|
98
134
|
# Transactions aren't supported
|
99
135
|
end
|
100
136
|
end
|
101
|
-
|
137
|
+
|
102
138
|
def quote_column_name(name)
|
103
139
|
return "`#{name}`"
|
104
140
|
end
|
105
|
-
|
141
|
+
|
142
|
+
def adapter_name()
|
143
|
+
'MySQL'
|
144
|
+
end
|
145
|
+
|
106
146
|
def structure_dump
|
107
147
|
select_all("SHOW TABLES").inject("") do |structure, table|
|
108
148
|
structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
|
109
149
|
end
|
110
150
|
end
|
111
|
-
|
151
|
+
|
112
152
|
def recreate_database(name)
|
113
153
|
drop_database(name)
|
114
154
|
create_database(name)
|
115
155
|
end
|
116
|
-
|
156
|
+
|
117
157
|
def drop_database(name)
|
118
158
|
execute "DROP DATABASE IF EXISTS #{name}"
|
119
159
|
end
|
120
|
-
|
160
|
+
|
121
161
|
def create_database(name)
|
122
162
|
execute "CREATE DATABASE #{name}"
|
123
163
|
end
|
124
|
-
|
164
|
+
|
125
165
|
def quote_string(s)
|
126
166
|
Mysql::quote(s)
|
127
167
|
end
|
128
|
-
|
168
|
+
|
129
169
|
private
|
130
170
|
def select(sql, name = nil)
|
131
171
|
result = nil
|
132
|
-
|
172
|
+
@connection.query_with_result = true
|
173
|
+
result = execute(sql, name)
|
133
174
|
rows = []
|
134
175
|
all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
|
135
176
|
result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
|
@@ -137,4 +178,4 @@ module ActiveRecord
|
|
137
178
|
end
|
138
179
|
end
|
139
180
|
end
|
140
|
-
end
|
181
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
# Implementation notes:
|
2
|
+
# 1. I had to redefine a method in ActiveRecord to make it possible to implement an autonumbering
|
3
|
+
# solution for oracle. It's implemented in a way that is intended to not break other adapters.
|
4
|
+
# 2. Default value support needs a patch to the OCI8 driver, to enable it to read LONG columns.
|
5
|
+
# LONG is deprecated, and so may never be properly added to driver.
|
6
|
+
# A similar patch is needed for TIMESTAMP.
|
7
|
+
# This is dangerous because it may break with newer versions of the driver.
|
8
|
+
# 3. Large Object support works by an after_save callback added to the ActiveRecord. This is not
|
9
|
+
# a problem - you can add other (chained) after_save callbacks.
|
10
|
+
# 4. LIMIT and OFFSET work by scrolling through a cursor - no rownum select from select required.
|
11
|
+
# It does mean that large OFFSETs will have to scroll through the intervening records. To keep
|
12
|
+
# consistency with other adapters I've allowed the LIMIT and OFFSET clauses to be included in
|
13
|
+
# the sql string and later extracted them by parsing the string.
|
14
|
+
#
|
15
|
+
# Do what you want with this code, at your own peril, but if any significant portion of my code
|
16
|
+
# remains then please acknowledge my contribution.
|
17
|
+
# Copyright 2005 Graham Jenkins
|
18
|
+
# $Revision: 1.2 $
|
19
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
20
|
+
|
21
|
+
begin
|
22
|
+
require_library_or_gem 'oci8' unless self.class.const_defined? :OCI8
|
23
|
+
|
24
|
+
module ActiveRecord
|
25
|
+
module ConnectionAdapters #:nodoc:
|
26
|
+
class OCIColumn < Column #:nodoc:
|
27
|
+
attr_reader :sql_type
|
28
|
+
|
29
|
+
def initialize(name, default, limit, sql_type, scale)
|
30
|
+
@name, @limit, @sql_type, @scale, @sequence = name, limit, sql_type, scale
|
31
|
+
@type = simplified_type sql_type
|
32
|
+
@default = type_cast default
|
33
|
+
end
|
34
|
+
|
35
|
+
def simplified_type(field_type)
|
36
|
+
case field_type
|
37
|
+
when /char/i : :string
|
38
|
+
when /num|float|double|dec|real|int/i : @scale == 0 ? :integer : :float
|
39
|
+
when /date|time/i : @name =~ /_at$/ ? :time : :datetime
|
40
|
+
when /lob/i : :binary
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def type_cast(value)
|
45
|
+
return nil if value.nil? || value =~ /^\s*null\s*$/i
|
46
|
+
case type
|
47
|
+
when :string then value
|
48
|
+
when :integer then value.to_i
|
49
|
+
when :float then value.to_f
|
50
|
+
when :datetime then cast_to_date_or_time(value)
|
51
|
+
when :time then cast_to_time(value)
|
52
|
+
else value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def cast_to_date_or_time(value)
|
57
|
+
return value if value.is_a? Date
|
58
|
+
guess_date_or_time (value.is_a? Time) ? value : cast_to_time(value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def cast_to_time(value)
|
62
|
+
return value if value.is_a? Time
|
63
|
+
time_array = ParseDate.parsedate value
|
64
|
+
time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1;
|
65
|
+
Time.send Base.default_timezone, *time_array
|
66
|
+
end
|
67
|
+
|
68
|
+
def guess_date_or_time(value)
|
69
|
+
(value.hour == 0 and value.min == 0 and value.sec == 0) ?
|
70
|
+
Date.new(value.year, value.month, value.day) : value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# This is an Oracle adapter for the ActiveRecord persistence framework. It relies upon the OCI8
|
75
|
+
# driver (http://rubyforge.org/projects/ruby-oci8/), which works with Oracle 8i and above.
|
76
|
+
# It was developed on Windows 2000 against an 8i database, using ActiveRecord 1.6.0 and OCI8 0.1.9.
|
77
|
+
# It has also been tested against a 9i database.
|
78
|
+
#
|
79
|
+
# Usage notes:
|
80
|
+
# * Key generation uses a sequence "rails_sequence" for all tables. (I couldn't find a simple
|
81
|
+
# and safe way of passing table-specific sequence information to the adapter.)
|
82
|
+
# * Oracle uses DATE or TIMESTAMP datatypes for both dates and times. Consequently I have had to
|
83
|
+
# resort to some hacks to get data converted to Date or Time in Ruby.
|
84
|
+
# If the column_name ends in _time it's created as a Ruby Time. Else if the
|
85
|
+
# hours/minutes/seconds are 0, I make it a Ruby Date. Else it's a Ruby Time.
|
86
|
+
# This is nasty - but if you use Duck Typing you'll probably not care very much.
|
87
|
+
# In 9i it's tempting to map DATE to Date and TIMESTAMP to Time but I don't think that is
|
88
|
+
# valid - too many databases use DATE for both.
|
89
|
+
# Timezones and sub-second precision on timestamps are not supported.
|
90
|
+
# * Default values that are functions (such as "SYSDATE") are not supported. This is a
|
91
|
+
# restriction of the way active record supports default values.
|
92
|
+
# * Referential integrity constraints are not fully supported. Under at least
|
93
|
+
# some circumstances, active record appears to delete parent and child records out of
|
94
|
+
# sequence and out of transaction scope. (Or this may just be a problem of test setup.)
|
95
|
+
#
|
96
|
+
# Options:
|
97
|
+
#
|
98
|
+
# * <tt>:username</tt> -- Defaults to root
|
99
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
100
|
+
# * <tt>:host</tt> -- Defaults to localhost
|
101
|
+
class OCIAdapter < AbstractAdapter
|
102
|
+
def quote_string(s)
|
103
|
+
s.gsub /'/, "''"
|
104
|
+
end
|
105
|
+
|
106
|
+
def quote(value, column = nil)
|
107
|
+
if column and column.type == :binary then %Q{empty_#{ column.sql_type }()}
|
108
|
+
else case value
|
109
|
+
when String then %Q{'#{quote_string(value)}'}
|
110
|
+
when NilClass then 'null'
|
111
|
+
when TrueClass then '1'
|
112
|
+
when FalseClass then '0'
|
113
|
+
when Numeric then value.to_s
|
114
|
+
when Date, Time then %Q{'#{value.strftime("%Y-%m-%d %H:%M:%S")}'}
|
115
|
+
else %Q{'#{quote_string(value.to_yaml)}'}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def select_all(sql, name = nil)
|
121
|
+
offset = sql =~ /OFFSET (\d+)$/ ? $1.to_i : -1
|
122
|
+
sql, limit = $1, $2.to_i if sql =~ /(.*)(?: LIMIT[= ](\d+))(\s*OFFSET \d+)?$/
|
123
|
+
cursor = log(sql, name, @connection) { @connection.exec sql }
|
124
|
+
cols = cursor.get_col_names.map { |x| x.downcase }
|
125
|
+
rows = []
|
126
|
+
while row = cursor.fetch
|
127
|
+
next if cursor.row_count <= offset
|
128
|
+
hash = Hash.new
|
129
|
+
cols.each_with_index { |col, i|
|
130
|
+
hash[col] = case row[i]
|
131
|
+
when OCI8::LOB
|
132
|
+
name == 'Writable Large Object' ? row[i]: row[i].read
|
133
|
+
when OraDate
|
134
|
+
(row[i].hour == 0 and row[i].minute == 0 and row[i].second == 0) ?
|
135
|
+
row[i].to_date : row[i].to_time
|
136
|
+
else row[i]
|
137
|
+
end
|
138
|
+
}
|
139
|
+
rows << hash
|
140
|
+
break if rows.size == limit
|
141
|
+
end
|
142
|
+
rows
|
143
|
+
ensure
|
144
|
+
cursor.close if cursor
|
145
|
+
end
|
146
|
+
|
147
|
+
def select_one(sql, name = nil)
|
148
|
+
result = select_all sql, name
|
149
|
+
result.size > 0 ? result.first : nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def columns(table_name, name = nil)
|
153
|
+
cols = select_all(%Q{
|
154
|
+
select column_name, data_type, data_default, data_length, data_scale
|
155
|
+
from user_tab_columns where table_name = '#{table_name.upcase}'}
|
156
|
+
).map { |row|
|
157
|
+
OCIColumn.new row['column_name'].downcase, row['data_default'],
|
158
|
+
row['data_length'], row['data_type'], row['data_scale']
|
159
|
+
}
|
160
|
+
cols
|
161
|
+
end
|
162
|
+
|
163
|
+
def insert(sql, name = nil, pk = nil, id_value = nil)
|
164
|
+
if pk.nil? # Who called us? What does the sql look like? No idea!
|
165
|
+
execute sql, name
|
166
|
+
elsif id_value # Pre-assigned id
|
167
|
+
log(sql, name, @connection) { @connection.exec sql }
|
168
|
+
else # Assume the sql contains a bind-variable for the id
|
169
|
+
id_value = select_one("select rails_sequence.nextval id from dual")['id']
|
170
|
+
log(sql, name, @connection) { @connection.exec sql, id_value }
|
171
|
+
end
|
172
|
+
id_value
|
173
|
+
end
|
174
|
+
|
175
|
+
def execute(sql, name = nil)
|
176
|
+
log(sql, name, @connection) { @connection.exec sql }
|
177
|
+
end
|
178
|
+
|
179
|
+
alias :update :execute
|
180
|
+
alias :delete :execute
|
181
|
+
|
182
|
+
def add_limit!(sql, limit)
|
183
|
+
sql << "LIMIT=" << limit.to_s
|
184
|
+
end
|
185
|
+
|
186
|
+
def begin_db_transaction()
|
187
|
+
@connection.autocommit = false
|
188
|
+
end
|
189
|
+
|
190
|
+
def commit_db_transaction()
|
191
|
+
@connection.commit
|
192
|
+
ensure
|
193
|
+
@connection.autocommit = true
|
194
|
+
end
|
195
|
+
|
196
|
+
def rollback_db_transaction()
|
197
|
+
@connection.rollback
|
198
|
+
ensure
|
199
|
+
@connection.autocommit = true
|
200
|
+
end
|
201
|
+
|
202
|
+
def adapter_name()
|
203
|
+
'OCI'
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
module ActiveRecord
|
210
|
+
class Base
|
211
|
+
def self.oci_connection(config) #:nodoc:
|
212
|
+
conn = OCI8.new config[:username], config[:password], config[:host]
|
213
|
+
conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
|
214
|
+
conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'}
|
215
|
+
conn.autocommit = true
|
216
|
+
ConnectionAdapters::OCIAdapter.new conn, logger
|
217
|
+
end
|
218
|
+
|
219
|
+
alias :attributes_with_quotes_pre_oci :attributes_with_quotes #:nodoc:
|
220
|
+
# Enable the id column to be bound into the sql later, by the adapter's insert method.
|
221
|
+
# This is preferable to inserting the hard-coded value here, because the insert method
|
222
|
+
# needs to know the id value explicitly.
|
223
|
+
def attributes_with_quotes(creating = true) #:nodoc:
|
224
|
+
aq = attributes_with_quotes_pre_oci creating
|
225
|
+
if connection.class == ConnectionAdapters::OCIAdapter
|
226
|
+
aq[self.class.primary_key] = ":id" if creating && aq[self.class.primary_key].nil?
|
227
|
+
end
|
228
|
+
aq
|
229
|
+
end
|
230
|
+
|
231
|
+
after_save :write_lobs
|
232
|
+
|
233
|
+
# After setting large objects to empty, select the OCI8::LOB and write back the data
|
234
|
+
def write_lobs() #:nodoc:
|
235
|
+
if connection.class == ConnectionAdapters::OCIAdapter
|
236
|
+
self.class.columns.select { |c| c.type == :binary }.each { |c|
|
237
|
+
break unless value = self[c.name]
|
238
|
+
lob = connection.select_one(
|
239
|
+
"select #{ c.name} from #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}",
|
240
|
+
'Writable Large Object'
|
241
|
+
)[c.name]
|
242
|
+
lob.write value
|
243
|
+
}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
private :write_lobs
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class OCI8 #:nodoc:
|
252
|
+
class Cursor #:nodoc:
|
253
|
+
alias :define_a_column_pre_ar :define_a_column
|
254
|
+
def define_a_column(i)
|
255
|
+
case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
|
256
|
+
when 8 : @stmt.defineByPos(i, String, 65535) # Read LONG values
|
257
|
+
when 187 : @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
|
258
|
+
else define_a_column_pre_ar i
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
rescue LoadError
|
264
|
+
# OCI8 driver is unavailable.
|
265
|
+
end
|