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.

Files changed (96) hide show
  1. data/CHANGELOG +78 -0
  2. data/README +20 -29
  3. data/RUNNING_UNIT_TESTS +1 -2
  4. data/examples/validation.rb +0 -3
  5. data/install.rb +3 -16
  6. data/lib/active_record.rb +11 -4
  7. data/lib/active_record/aggregations.rb +2 -2
  8. data/lib/active_record/associations.rb +8 -8
  9. data/lib/active_record/associations/association_collection.rb +1 -1
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -1
  11. data/lib/active_record/base.rb +117 -43
  12. data/lib/active_record/callbacks.rb +2 -2
  13. data/lib/active_record/connection_adapters/abstract_adapter.rb +7 -14
  14. data/lib/active_record/connection_adapters/db2_adapter.rb +33 -22
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +74 -33
  16. data/lib/active_record/connection_adapters/oci_adapter.rb +265 -0
  17. data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -3
  18. data/lib/active_record/connection_adapters/sqlite_adapter.rb +13 -4
  19. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +158 -67
  20. data/lib/active_record/deprecated_associations.rb +4 -4
  21. data/lib/active_record/fixtures.rb +12 -5
  22. data/lib/active_record/locking.rb +22 -22
  23. data/lib/active_record/observer.rb +6 -3
  24. data/lib/active_record/timestamp.rb +15 -5
  25. data/lib/active_record/transactions.rb +4 -4
  26. data/lib/active_record/validations.rb +272 -189
  27. data/lib/active_record/wrappings.rb +2 -2
  28. data/rakefile +17 -2
  29. data/test/aaa_create_tables_test.rb +58 -0
  30. data/test/abstract_unit.rb +3 -2
  31. data/test/aggregations_test.rb +0 -1
  32. data/test/associations_test.rb +27 -28
  33. data/test/base_test.rb +74 -2
  34. data/test/binary_test.rb +6 -2
  35. data/test/class_inheritable_attributes_test.rb +1 -1
  36. data/test/column_alias_test.rb +9 -2
  37. data/test/connections/native_oci/connection.rb +25 -0
  38. data/test/connections/native_sqlite/connection.rb +4 -1
  39. data/test/connections/native_sqlite3/connection.rb +4 -2
  40. data/test/deprecated_associations_test.rb +4 -5
  41. data/test/finder_test.rb +20 -4
  42. data/test/fixtures/db_definitions/create_oracle_db.bat +5 -0
  43. data/test/fixtures/db_definitions/create_oracle_db.sh +5 -0
  44. data/test/fixtures/db_definitions/db2.drop.sql +18 -0
  45. data/test/fixtures/db_definitions/db2.sql +1 -0
  46. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  47. data/test/fixtures/db_definitions/db22.sql +1 -0
  48. data/test/fixtures/db_definitions/drop_oracle_tables.sql +35 -0
  49. data/test/fixtures/db_definitions/drop_oracle_tables2.sql +3 -0
  50. data/test/fixtures/db_definitions/mysql.drop.sql +18 -0
  51. data/test/fixtures/db_definitions/mysql.sql +2 -1
  52. data/test/fixtures/db_definitions/mysql2.drop.sql +2 -0
  53. data/test/fixtures/db_definitions/mysql2.sql +1 -0
  54. data/test/fixtures/db_definitions/oci.drop.sql +18 -0
  55. data/test/fixtures/db_definitions/oci.sql +167 -0
  56. data/test/fixtures/db_definitions/oci2.drop.sql +2 -0
  57. data/test/fixtures/db_definitions/oci2.sql +6 -0
  58. data/test/fixtures/db_definitions/postgresql.drop.sql +18 -0
  59. data/test/fixtures/db_definitions/postgresql.sql +2 -1
  60. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  61. data/test/fixtures/db_definitions/postgresql2.sql +2 -1
  62. data/test/fixtures/db_definitions/sqlite.drop.sql +18 -0
  63. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  64. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  65. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  66. data/test/fixtures/db_definitions/sqlserver.drop.sql +18 -0
  67. data/test/fixtures/db_definitions/sqlserver.sql +1 -0
  68. data/test/fixtures/db_definitions/sqlserver2.drop.sql +2 -0
  69. data/test/fixtures/db_definitions/sqlserver2.sql +1 -0
  70. data/test/fixtures/fixture_database.sqlite +0 -0
  71. data/test/fixtures/fixture_database_2.sqlite +0 -0
  72. data/test/fixtures/topics.yml +3 -3
  73. data/test/lifecycle_test.rb +0 -1
  74. data/test/modules_test.rb +0 -1
  75. data/test/reflection_test.rb +0 -1
  76. data/test/validations_test.rb +229 -41
  77. metadata +36 -28
  78. data/dev-utils/eval_debugger.rb +0 -14
  79. data/lib/active_record/support/binding_of_caller.rb +0 -83
  80. data/lib/active_record/support/breakpoint.rb +0 -518
  81. data/lib/active_record/support/class_attribute_accessors.rb +0 -57
  82. data/lib/active_record/support/class_inheritable_attributes.rb +0 -117
  83. data/lib/active_record/support/clean_logger.rb +0 -10
  84. data/lib/active_record/support/core_ext.rb +0 -1
  85. data/lib/active_record/support/core_ext/hash.rb +0 -5
  86. data/lib/active_record/support/core_ext/hash/keys.rb +0 -35
  87. data/lib/active_record/support/core_ext/numeric.rb +0 -7
  88. data/lib/active_record/support/core_ext/numeric/bytes.rb +0 -33
  89. data/lib/active_record/support/core_ext/numeric/time.rb +0 -59
  90. data/lib/active_record/support/core_ext/object_and_class.rb +0 -24
  91. data/lib/active_record/support/core_ext/string.rb +0 -5
  92. data/lib/active_record/support/core_ext/string/inflections.rb +0 -45
  93. data/lib/active_record/support/dependencies.rb +0 -63
  94. data/lib/active_record/support/inflector.rb +0 -84
  95. data/lib/active_record/support/misc.rb +0 -8
  96. 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 lifecyle.
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 unvieled
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
- # Rollsback the transaction (and turns on auto-committing). Must be done if the transaction block
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
- # db2_adapter.rb
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
- class DB2Adapter < AbstractAdapter # :nodoc:
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
- def last_insert_id
104
- row = select_one(<<-GETID.strip)
105
- with temp(id) as (values (identity_val_local())) select * from temp
106
- GETID
107
- row['id'].to_i
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
- def select(sql, name = nil)
111
- stmt = nil
112
- log(sql, name, @connection) do |connection|
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
- ConnectionAdapters::MysqlAdapter.new(
36
- Mysql::real_connect(host, username, password, database, port, socket), logger
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
- class MysqlAdapter < AbstractAdapter # :nodoc:
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
- result.each { |field| columns << Column.new(field[0], field[4], field[1]) }
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
- log(sql, name, @connection) { |connection| connection.query(sql) }
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
- log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) }
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