activerecord 1.0.0 → 3.0.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 (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -1,131 +1,643 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
- require 'parsedate'
3
-
4
- begin
5
- begin
6
- # Only include the MySQL driver if one hasn't already been loaded
7
- require 'mysql' unless self.class.const_defined?(:Mysql)
8
- rescue LoadError
9
- # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
10
- require 'active_record/vendor/mysql'
11
- end
2
+ require 'active_support/core_ext/kernel/requires'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'set'
12
5
 
13
6
  module ActiveRecord
14
7
  class Base
15
- # Establishes a connection to the database that's used by all Active Record objects
8
+ # Establishes a connection to the database that's used by all Active Record objects.
16
9
  def self.mysql_connection(config) # :nodoc:
17
- symbolize_strings_in_hash(config)
10
+ config = config.symbolize_keys
18
11
  host = config[:host]
19
12
  port = config[:port]
20
13
  socket = config[:socket]
21
- username = config[:username] || "root"
22
- password = config[:password]
14
+ username = config[:username] ? config[:username].to_s : 'root'
15
+ password = config[:password].to_s
16
+ database = config[:database]
17
+
18
+ unless defined? Mysql
19
+ begin
20
+ require 'mysql'
21
+ rescue LoadError
22
+ raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'"
23
+ end
23
24
 
24
- if config.has_key?(:database)
25
- database = config[:database]
26
- else
27
- raise ArgumentError, "No database specified. Missing argument: database."
25
+ unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
26
+ raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'"
27
+ end
28
28
  end
29
29
 
30
- ConnectionAdapters::MysqlAdapter.new(
31
- Mysql::real_connect(host, username, password, database, port, socket), logger
32
- )
30
+ mysql = Mysql.init
31
+ mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
32
+
33
+ default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
34
+ default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
35
+ options = [host, username, password, database, port, socket, default_flags]
36
+ ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
33
37
  end
34
38
  end
35
39
 
36
40
  module ConnectionAdapters
37
- class MysqlAdapter < AbstractAdapter # :nodoc:
38
- def select_all(sql, name = nil)
39
- select(sql, name)
41
+ class MysqlColumn < Column #:nodoc:
42
+ def extract_default(default)
43
+ if sql_type =~ /blob/i || type == :text
44
+ if default.blank?
45
+ return null ? nil : ''
46
+ else
47
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
48
+ end
49
+ elsif missing_default_forged_as_empty_string?(default)
50
+ nil
51
+ else
52
+ super
53
+ end
40
54
  end
41
55
 
42
- def select_one(sql, name = nil)
43
- result = select(sql, name)
44
- result.nil? ? nil : result.first
56
+ def has_default?
57
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
58
+ super
45
59
  end
46
60
 
47
- def columns(table_name, name = nil)
48
- sql = "SHOW FIELDS FROM #{table_name}"
49
- result = nil
50
- log(sql, name, @connection) { |connection| result = connection.query(sql) }
61
+ private
62
+ def simplified_type(field_type)
63
+ return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
64
+ return :string if field_type =~ /enum/i
65
+ super
66
+ end
51
67
 
52
- columns = []
53
- result.each { |field| columns << Column.new(field[0], field[4], field[1]) }
54
- columns
68
+ def extract_limit(sql_type)
69
+ case sql_type
70
+ when /blob|text/i
71
+ case sql_type
72
+ when /tiny/i
73
+ 255
74
+ when /medium/i
75
+ 16777215
76
+ when /long/i
77
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
78
+ else
79
+ super # we could return 65535 here, but we leave it undecorated by default
80
+ end
81
+ when /^bigint/i; 8
82
+ when /^int/i; 4
83
+ when /^mediumint/i; 3
84
+ when /^smallint/i; 2
85
+ when /^tinyint/i; 1
86
+ else
87
+ super
88
+ end
89
+ end
90
+
91
+ # MySQL misreports NOT NULL column default when none is given.
92
+ # We can't detect this for columns which may have a legitimate ''
93
+ # default (string) but we can for others (integer, datetime, boolean,
94
+ # and the rest).
95
+ #
96
+ # Test whether the column has default '', is not null, and is not
97
+ # a type allowing default ''.
98
+ def missing_default_forged_as_empty_string?(default)
99
+ type != :string && !null && default == ''
100
+ end
101
+ end
102
+
103
+ # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
104
+ # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
105
+ #
106
+ # Options:
107
+ #
108
+ # * <tt>:host</tt> - Defaults to "localhost".
109
+ # * <tt>:port</tt> - Defaults to 3306.
110
+ # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
111
+ # * <tt>:username</tt> - Defaults to "root"
112
+ # * <tt>:password</tt> - Defaults to nothing.
113
+ # * <tt>:database</tt> - The name of the database. No default, must be provided.
114
+ # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
115
+ # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
116
+ # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
117
+ # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
118
+ # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
119
+ # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
120
+ # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
121
+ #
122
+ class MysqlAdapter < AbstractAdapter
123
+
124
+ ##
125
+ # :singleton-method:
126
+ # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
127
+ # as boolean. If you wish to disable this emulation (which was the default
128
+ # behavior in versions 0.13.1 and earlier) you can add the following line
129
+ # to your application.rb file:
130
+ #
131
+ # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
132
+ cattr_accessor :emulate_booleans
133
+ self.emulate_booleans = true
134
+
135
+ ADAPTER_NAME = 'MySQL'.freeze
136
+
137
+ LOST_CONNECTION_ERROR_MESSAGES = [
138
+ "Server shutdown in progress",
139
+ "Broken pipe",
140
+ "Lost connection to MySQL server during query",
141
+ "MySQL server has gone away" ]
142
+
143
+ QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
144
+
145
+ NATIVE_DATABASE_TYPES = {
146
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
147
+ :string => { :name => "varchar", :limit => 255 },
148
+ :text => { :name => "text" },
149
+ :integer => { :name => "int", :limit => 4 },
150
+ :float => { :name => "float" },
151
+ :decimal => { :name => "decimal" },
152
+ :datetime => { :name => "datetime" },
153
+ :timestamp => { :name => "datetime" },
154
+ :time => { :name => "time" },
155
+ :date => { :name => "date" },
156
+ :binary => { :name => "blob" },
157
+ :boolean => { :name => "tinyint", :limit => 1 }
158
+ }
159
+
160
+ def initialize(connection, logger, connection_options, config)
161
+ super(connection, logger)
162
+ @connection_options, @config = connection_options, config
163
+ @quoted_column_names, @quoted_table_names = {}, {}
164
+ connect
55
165
  end
56
166
 
57
- def insert(sql, name = nil, pk = nil, id_value = nil)
58
- execute(sql, name = nil)
59
- return id_value || @connection.insert_id
167
+ def adapter_name #:nodoc:
168
+ ADAPTER_NAME
60
169
  end
61
170
 
62
- def execute(sql, name = nil)
63
- log(sql, name, @connection) { |connection| connection.query(sql) }
171
+ def supports_migrations? #:nodoc:
172
+ true
64
173
  end
65
174
 
66
- alias_method :update, :execute
67
- alias_method :delete, :execute
68
-
69
- def begin_db_transaction
70
- begin
71
- execute "BEGIN"
72
- rescue Exception
73
- # Transactions aren't supported
175
+ def supports_primary_key? #:nodoc:
176
+ true
177
+ end
178
+
179
+ def supports_savepoints? #:nodoc:
180
+ true
181
+ end
182
+
183
+ def native_database_types #:nodoc:
184
+ NATIVE_DATABASE_TYPES
185
+ end
186
+
187
+
188
+ # QUOTING ==================================================
189
+
190
+ def quote(value, column = nil)
191
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
192
+ s = column.class.string_to_binary(value).unpack("H*")[0]
193
+ "x'#{s}'"
194
+ elsif value.kind_of?(BigDecimal)
195
+ value.to_s("F")
196
+ else
197
+ super
74
198
  end
75
199
  end
76
-
77
- def commit_db_transaction
200
+
201
+ def quote_column_name(name) #:nodoc:
202
+ @quoted_column_names[name] ||= "`#{name}`"
203
+ end
204
+
205
+ def quote_table_name(name) #:nodoc:
206
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
207
+ end
208
+
209
+ def quote_string(string) #:nodoc:
210
+ @connection.quote(string)
211
+ end
212
+
213
+ def quoted_true
214
+ QUOTED_TRUE
215
+ end
216
+
217
+ def quoted_false
218
+ QUOTED_FALSE
219
+ end
220
+
221
+ # REFERENTIAL INTEGRITY ====================================
222
+
223
+ def disable_referential_integrity #:nodoc:
224
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
225
+
78
226
  begin
79
- execute "COMMIT"
80
- rescue Exception
81
- # Transactions aren't supported
227
+ update("SET FOREIGN_KEY_CHECKS = 0")
228
+ yield
229
+ ensure
230
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
82
231
  end
83
232
  end
84
-
85
- def rollback_db_transaction
86
- begin
87
- execute "ROLLBACK"
88
- rescue Exception
89
- # Transactions aren't supported
233
+
234
+ # CONNECTION MANAGEMENT ====================================
235
+
236
+ def active?
237
+ if @connection.respond_to?(:stat)
238
+ @connection.stat
239
+ else
240
+ @connection.query 'select 1'
241
+ end
242
+
243
+ # mysql-ruby doesn't raise an exception when stat fails.
244
+ if @connection.respond_to?(:errno)
245
+ @connection.errno.zero?
246
+ else
247
+ true
248
+ end
249
+ rescue Mysql::Error
250
+ false
251
+ end
252
+
253
+ def reconnect!
254
+ disconnect!
255
+ connect
256
+ end
257
+
258
+ def disconnect!
259
+ @connection.close rescue nil
260
+ end
261
+
262
+ def reset!
263
+ if @connection.respond_to?(:change_user)
264
+ # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
265
+ # reset the connection is to change the user to the same user.
266
+ @connection.change_user(@config[:username], @config[:password], @config[:database])
267
+ configure_connection
90
268
  end
91
269
  end
92
270
 
93
- def quote_column_name(name)
94
- return "`#{name}`"
271
+ # DATABASE STATEMENTS ======================================
272
+
273
+ def select_rows(sql, name = nil)
274
+ @connection.query_with_result = true
275
+ result = execute(sql, name)
276
+ rows = []
277
+ result.each { |row| rows << row }
278
+ result.free
279
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
280
+ rows
95
281
  end
96
-
97
- def structure_dump
98
- select_all("SHOW TABLES").inject("") do |structure, table|
99
- structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
282
+
283
+ # Executes an SQL query and returns a MySQL::Result object. Note that you have to free
284
+ # the Result object after you're done using it.
285
+ def execute(sql, name = nil) #:nodoc:
286
+ if name == :skip_logging
287
+ @connection.query(sql)
288
+ else
289
+ log(sql, name) { @connection.query(sql) }
290
+ end
291
+ rescue ActiveRecord::StatementInvalid => exception
292
+ if exception.message.split(":").first =~ /Packets out of order/
293
+ raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
294
+ else
295
+ raise
100
296
  end
101
297
  end
102
-
103
- def recreate_database(name)
298
+
299
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
300
+ super sql, name
301
+ id_value || @connection.insert_id
302
+ end
303
+ alias :create :insert_sql
304
+
305
+ def update_sql(sql, name = nil) #:nodoc:
306
+ super
307
+ @connection.affected_rows
308
+ end
309
+
310
+ def begin_db_transaction #:nodoc:
311
+ execute "BEGIN"
312
+ rescue Exception
313
+ # Transactions aren't supported
314
+ end
315
+
316
+ def commit_db_transaction #:nodoc:
317
+ execute "COMMIT"
318
+ rescue Exception
319
+ # Transactions aren't supported
320
+ end
321
+
322
+ def rollback_db_transaction #:nodoc:
323
+ execute "ROLLBACK"
324
+ rescue Exception
325
+ # Transactions aren't supported
326
+ end
327
+
328
+ def create_savepoint
329
+ execute("SAVEPOINT #{current_savepoint_name}")
330
+ end
331
+
332
+ def rollback_to_savepoint
333
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
334
+ end
335
+
336
+ def release_savepoint
337
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
338
+ end
339
+
340
+ def add_limit_offset!(sql, options) #:nodoc:
341
+ limit, offset = options[:limit], options[:offset]
342
+ if limit && offset
343
+ sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
344
+ elsif limit
345
+ sql << " LIMIT #{sanitize_limit(limit)}"
346
+ elsif offset
347
+ sql << " OFFSET #{offset.to_i}"
348
+ end
349
+ sql
350
+ end
351
+
352
+ # SCHEMA STATEMENTS ========================================
353
+
354
+ def structure_dump #:nodoc:
355
+ if supports_views?
356
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
357
+ else
358
+ sql = "SHOW TABLES"
359
+ end
360
+
361
+ select_all(sql).inject("") do |structure, table|
362
+ table.delete('Table_type')
363
+ structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
364
+ end
365
+ end
366
+
367
+ def recreate_database(name, options = {}) #:nodoc:
104
368
  drop_database(name)
105
- create_database(name)
369
+ create_database(name, options)
370
+ end
371
+
372
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
373
+ # Charset defaults to utf8.
374
+ #
375
+ # Example:
376
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
377
+ # create_database 'matt_development'
378
+ # create_database 'matt_development', :charset => :big5
379
+ def create_database(name, options = {})
380
+ if options[:collation]
381
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
382
+ else
383
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
384
+ end
385
+ end
386
+
387
+ def drop_database(name) #:nodoc:
388
+ execute "DROP DATABASE IF EXISTS `#{name}`"
389
+ end
390
+
391
+ def current_database
392
+ select_value 'SELECT DATABASE() as db'
393
+ end
394
+
395
+ # Returns the database character set.
396
+ def charset
397
+ show_variable 'character_set_database'
398
+ end
399
+
400
+ # Returns the database collation strategy.
401
+ def collation
402
+ show_variable 'collation_database'
403
+ end
404
+
405
+ def tables(name = nil) #:nodoc:
406
+ tables = []
407
+ result = execute("SHOW TABLES", name)
408
+ result.each { |field| tables << field[0] }
409
+ result.free
410
+ tables
411
+ end
412
+
413
+ def drop_table(table_name, options = {})
414
+ super(table_name, options)
415
+ end
416
+
417
+ def indexes(table_name, name = nil)#:nodoc:
418
+ indexes = []
419
+ current_index = nil
420
+ result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
421
+ result.each do |row|
422
+ if current_index != row[2]
423
+ next if row[2] == "PRIMARY" # skip the primary key
424
+ current_index = row[2]
425
+ indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
426
+ end
427
+
428
+ indexes.last.columns << row[4]
429
+ indexes.last.lengths << row[7]
430
+ end
431
+ result.free
432
+ indexes
433
+ end
434
+
435
+ def columns(table_name, name = nil)#:nodoc:
436
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
437
+ columns = []
438
+ result = execute(sql, :skip_logging)
439
+ result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
440
+ result.free
441
+ columns
442
+ end
443
+
444
+ def create_table(table_name, options = {}) #:nodoc:
445
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
446
+ end
447
+
448
+ def rename_table(table_name, new_name)
449
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
450
+ end
451
+
452
+ def add_column(table_name, column_name, type, options = {})
453
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
454
+ add_column_options!(add_column_sql, options)
455
+ add_column_position!(add_column_sql, options)
456
+ execute(add_column_sql)
457
+ end
458
+
459
+ def change_column_default(table_name, column_name, default) #:nodoc:
460
+ column = column_for(table_name, column_name)
461
+ change_column table_name, column_name, column.sql_type, :default => default
462
+ end
463
+
464
+ def change_column_null(table_name, column_name, null, default = nil)
465
+ column = column_for(table_name, column_name)
466
+
467
+ unless null || default.nil?
468
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
469
+ end
470
+
471
+ change_column table_name, column_name, column.sql_type, :null => null
472
+ end
473
+
474
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
475
+ column = column_for(table_name, column_name)
476
+
477
+ unless options_include_default?(options)
478
+ options[:default] = column.default
479
+ end
480
+
481
+ unless options.has_key?(:null)
482
+ options[:null] = column.null
483
+ end
484
+
485
+ change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
486
+ add_column_options!(change_column_sql, options)
487
+ add_column_position!(change_column_sql, options)
488
+ execute(change_column_sql)
489
+ end
490
+
491
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
492
+ options = {}
493
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
494
+ options[:default] = column.default
495
+ options[:null] = column.null
496
+ else
497
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
498
+ end
499
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
500
+ rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
501
+ add_column_options!(rename_column_sql, options)
502
+ execute(rename_column_sql)
503
+ end
504
+
505
+ # Maps logical Rails types to MySQL-specific data types.
506
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
507
+ return super unless type.to_s == 'integer'
508
+
509
+ case limit
510
+ when 1; 'tinyint'
511
+ when 2; 'smallint'
512
+ when 3; 'mediumint'
513
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
514
+ when 5..8; 'bigint'
515
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
516
+ end
517
+ end
518
+
519
+ def add_column_position!(sql, options)
520
+ if options[:first]
521
+ sql << " FIRST"
522
+ elsif options[:after]
523
+ sql << " AFTER #{quote_column_name(options[:after])}"
524
+ end
525
+ end
526
+
527
+ # SHOW VARIABLES LIKE 'name'
528
+ def show_variable(name)
529
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
530
+ variables.first['Value'] unless variables.empty?
106
531
  end
107
-
108
- def drop_database(name)
109
- execute "DROP DATABASE IF EXISTS #{name}"
532
+
533
+ # Returns a table's primary key and belonging sequence.
534
+ def pk_and_sequence_for(table) #:nodoc:
535
+ keys = []
536
+ result = execute("describe #{quote_table_name(table)}")
537
+ result.each_hash do |h|
538
+ keys << h["Field"]if h["Key"] == "PRI"
539
+ end
540
+ result.free
541
+ keys.length == 1 ? [keys.first, nil] : nil
542
+ end
543
+
544
+ # Returns just a table's primary key
545
+ def primary_key(table)
546
+ pk_and_sequence = pk_and_sequence_for(table)
547
+ pk_and_sequence && pk_and_sequence.first
110
548
  end
111
-
112
- def create_database(name)
113
- execute "CREATE DATABASE #{name}"
549
+
550
+ def case_sensitive_equality_operator
551
+ "= BINARY"
114
552
  end
115
-
553
+
554
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
555
+ where_sql
556
+ end
557
+
558
+ protected
559
+ def quoted_columns_for_index(column_names, options = {})
560
+ length = options[:length] if options.is_a?(Hash)
561
+
562
+ quoted_column_names = case length
563
+ when Hash
564
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
565
+ when Fixnum
566
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
567
+ else
568
+ column_names.map {|name| quote_column_name(name) }
569
+ end
570
+ end
571
+
572
+ def translate_exception(exception, message)
573
+ return super unless exception.respond_to?(:errno)
574
+
575
+ case exception.errno
576
+ when 1062
577
+ RecordNotUnique.new(message, exception)
578
+ when 1452
579
+ InvalidForeignKey.new(message, exception)
580
+ else
581
+ super
582
+ end
583
+ end
584
+
116
585
  private
586
+ def connect
587
+ encoding = @config[:encoding]
588
+ if encoding
589
+ @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
590
+ end
591
+
592
+ if @config[:sslca] || @config[:sslkey]
593
+ @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
594
+ end
595
+
596
+ @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
597
+ @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
598
+ @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
599
+
600
+ @connection.real_connect(*@connection_options)
601
+
602
+ # reconnect must be set after real_connect is called, because real_connect sets it to false internally
603
+ @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
604
+
605
+ configure_connection
606
+ end
607
+
608
+ def configure_connection
609
+ encoding = @config[:encoding]
610
+ execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
611
+
612
+ # By default, MySQL 'where id is null' selects the last inserted id.
613
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
614
+ execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
615
+ end
616
+
117
617
  def select(sql, name = nil)
118
- result = nil
119
- log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) }
618
+ @connection.query_with_result = true
619
+ result = execute(sql, name)
120
620
  rows = []
121
- all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
122
- result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
621
+ result.each_hash { |row| rows << row }
622
+ result.free
623
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
123
624
  rows
124
625
  end
626
+
627
+ def supports_views?
628
+ version[0] >= 5
629
+ end
630
+
631
+ def version
632
+ @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
633
+ end
634
+
635
+ def column_for(table_name, column_name)
636
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
637
+ raise "No such column: #{table_name}.#{column_name}"
638
+ end
639
+ column
640
+ end
125
641
  end
126
642
  end
127
643
  end
128
-
129
- rescue LoadError
130
- # MySQL is not available, so neither should the adapter be
131
- end