mysql2 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (April 23rd, 2010)
4
+ * optimization: implemented a local cache for rows that are lazily created in ruby during iteration. The MySQL C result is freed as soon as all the results have been cached
5
+ * optimization: implemented a local cache for field names so every row reuses the same objects as field names/keys
6
+ * refactor the Mysql2 connection adapter for ActiveRecord to not extend the Mysql adapter - now being a free-standing connection adapter
7
+
3
8
  ## 0.1.3 (April 15th, 2010)
4
9
  * added an EventMachine Deferrable API
5
10
  * added an ActiveRecord connection adapter
@@ -130,11 +130,11 @@ Me: Yep, but it's API is considerably more complex *and* is 2-3x slower.
130
130
  Performing a basic "SELECT * FROM" query on a table with 30k rows and fields of nearly every Ruby-representable data type,
131
131
  then iterating over every row using an #each like method yielding a block:
132
132
 
133
- # And remember, the Mysql gem only gives back nil and strings.
134
- user system total real
133
+ # And remember, the Mysql gem only gives back nil and strings for values.
134
+ user system total real
135
135
  Mysql2
136
- 2.080000 0.790000 2.870000 ( 3.418861)
136
+ 0.610000 0.160000 0.770000 ( 0.986967)
137
137
  Mysql
138
- 1.210000 0.790000 2.000000 ( 4.527824)
138
+ 0.350000 0.220000 0.570000 ( 1.457889)
139
139
  do_mysql
140
- 5.450000 0.980000 6.430000 ( 7.001563)
140
+ 1.710000 0.180000 1.890000 ( 2.124831)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.1.4
@@ -4,9 +4,8 @@ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
4
  require 'rubygems'
5
5
  require 'benchmark'
6
6
  require 'active_record'
7
- require 'ruby-debug'
8
7
 
9
- number_of = 1
8
+ number_of = 10
10
9
  mysql2_opts = {
11
10
  :adapter => 'mysql2',
12
11
  :database => 'test'
@@ -6,9 +6,9 @@ require 'mysql'
6
6
  require 'mysql2_ext'
7
7
  require 'do_mysql'
8
8
 
9
- number_of = 1
9
+ number_of = 100
10
10
  database = 'test'
11
- sql = "SELECT * FROM mysql2_test"
11
+ sql = "SELECT * FROM mysql2_test LIMIT 100"
12
12
 
13
13
  Benchmark.bmbm do |x|
14
14
  mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root")
@@ -269,28 +269,43 @@ static VALUE rb_mysql_client_affected_rows(VALUE self) {
269
269
  /* Mysql2::Result */
270
270
  static VALUE rb_mysql_result_to_obj(MYSQL_RES * r) {
271
271
  VALUE obj;
272
- obj = Data_Wrap_Struct(cMysql2Result, 0, rb_mysql_result_free, r);
272
+ mysql2_result_wrapper * wrapper;
273
+ obj = Data_Make_Struct(cMysql2Result, mysql2_result_wrapper, rb_mysql_result_mark, rb_mysql_result_free, wrapper);
274
+ wrapper->numberOfFields = 0;
275
+ wrapper->numberOfRows = 0;
276
+ wrapper->lastRowProcessed = 0;
277
+ wrapper->resultFreed = 0;
278
+ wrapper->result = r;
273
279
  rb_obj_call_init(obj, 0, NULL);
274
280
  return obj;
275
281
  }
276
282
 
277
- void rb_mysql_result_free(void * result) {
278
- MYSQL_RES * r = result;
279
- if (r) {
280
- mysql_free_result(r);
283
+ void rb_mysql_result_free(void * wrapper) {
284
+ mysql2_result_wrapper * w = wrapper;
285
+ if (w && w->resultFreed != 1) {
286
+ mysql_free_result(w->result);
287
+ w->resultFreed = 1;
281
288
  }
282
289
  }
283
290
 
291
+ void rb_mysql_result_mark(void * wrapper) {
292
+ mysql2_result_wrapper * w = wrapper;
293
+ if (w) {
294
+ rb_gc_mark(w->fields);
295
+ rb_gc_mark(w->rows);
296
+ }
297
+ }
298
+
284
299
  static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
285
300
  VALUE rowHash, opts, block;
286
- MYSQL_RES * result;
301
+ mysql2_result_wrapper * wrapper;
287
302
  MYSQL_ROW row;
288
- MYSQL_FIELD * fields;
303
+ MYSQL_FIELD * fields = NULL;
289
304
  struct tm parsedTime;
290
- unsigned int i = 0, numFields = 0, symbolizeKeys = 0;
305
+ unsigned int i = 0, symbolizeKeys = 0;
291
306
  unsigned long * fieldLengths;
292
307
 
293
- GetMysql2Result(self, result);
308
+ GetMysql2Result(self, wrapper);
294
309
 
295
310
  if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
296
311
  Check_Type(opts, T_HASH);
@@ -299,29 +314,39 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
299
314
  }
300
315
  }
301
316
 
302
- row = mysql_fetch_row(result);
317
+ row = mysql_fetch_row(wrapper->result);
303
318
  if (row == NULL) {
304
319
  return Qnil;
305
320
  }
306
321
 
307
- numFields = mysql_num_fields(result);
308
- fieldLengths = mysql_fetch_lengths(result);
309
- fields = mysql_fetch_fields(result);
322
+ if (wrapper->numberOfFields == 0) {
323
+ wrapper->numberOfFields = mysql_num_fields(wrapper->result);
324
+ wrapper->fields = rb_ary_new2(wrapper->numberOfFields);
325
+ }
310
326
 
311
327
  rowHash = rb_hash_new();
312
- for (i = 0; i < numFields; i++) {
313
- VALUE key;
314
- if (symbolizeKeys) {
315
- char buf[fields[i].name_length+1];
316
- memcpy(buf, fields[i].name, fields[i].name_length);
317
- buf[fields[i].name_length] = 0;
318
- key = ID2SYM(rb_intern(buf));
319
- } else {
320
- key = rb_str_new(fields[i].name, fields[i].name_length);
328
+ fields = mysql_fetch_fields(wrapper->result);
329
+ fieldLengths = mysql_fetch_lengths(wrapper->result);
330
+ for (i = 0; i < wrapper->numberOfFields; i++) {
331
+
332
+ // lazily create fields, but only once
333
+ // we'll use cached versions from here on out
334
+ VALUE field = rb_ary_entry(wrapper->fields, i);
335
+ if (field == Qnil) {
336
+ if (symbolizeKeys) {
337
+ char buf[fields[i].name_length+1];
338
+ memcpy(buf, fields[i].name, fields[i].name_length);
339
+ buf[fields[i].name_length] = 0;
340
+ field = ID2SYM(rb_intern(buf));
341
+ } else {
342
+ field = rb_str_new(fields[i].name, fields[i].name_length);
321
343
  #ifdef HAVE_RUBY_ENCODING_H
322
- rb_enc_associate_index(key, utf8Encoding);
344
+ rb_enc_associate_index(field, utf8Encoding);
323
345
  #endif
346
+ }
347
+ rb_ary_store(wrapper->fields, i, field);
324
348
  }
349
+
325
350
  if (row[i]) {
326
351
  VALUE val;
327
352
  switch(fields[i].type) {
@@ -393,54 +418,67 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
393
418
  #endif
394
419
  break;
395
420
  }
396
- rb_hash_aset(rowHash, key, val);
421
+ rb_hash_aset(rowHash, field, val);
397
422
  } else {
398
- rb_hash_aset(rowHash, key, Qnil);
423
+ rb_hash_aset(rowHash, field, Qnil);
399
424
  }
400
425
  }
401
426
  return rowHash;
402
427
  }
403
428
 
404
429
  static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
405
- VALUE dataset, opts, block;
406
- MYSQL_RES * result;
407
- unsigned long numRows, i;
430
+ VALUE opts, block;
431
+ mysql2_result_wrapper * wrapper;
432
+ unsigned long i;
408
433
 
409
- GetMysql2Result(self, result);
434
+ GetMysql2Result(self, wrapper);
410
435
 
411
436
  rb_scan_args(argc, argv, "01&", &opts, &block);
412
437
 
413
- // force-start at the beginning of the result set for proper
414
- // behavior of #each
415
- mysql_data_seek(result, 0);
416
-
417
- numRows = mysql_num_rows(result);
418
- if (numRows == 0) {
419
- return Qnil;
438
+ if (wrapper->lastRowProcessed == 0) {
439
+ wrapper->numberOfRows = mysql_num_rows(wrapper->result);
440
+ if (wrapper->numberOfRows == 0) {
441
+ return Qnil;
442
+ }
443
+ wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
420
444
  }
421
445
 
422
- // TODO: allow yielding datasets of configurable size
423
- // like find_in_batches from AR...
424
- if (block != Qnil) {
425
- for (i = 0; i < numRows; i++) {
426
- VALUE row = rb_mysql_result_fetch_row(argc, argv, self);
427
- if (row == Qnil) {
428
- return Qnil;
429
- }
430
- rb_yield(row);
446
+ if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
447
+ // we've already read the entire dataset from the C result into our
448
+ // internal array. Lets hand that over to the user since it's ready to go
449
+ for (i = 0; i < wrapper->numberOfRows; i++) {
450
+ rb_yield(rb_ary_entry(wrapper->rows, i));
431
451
  }
432
452
  } else {
433
- dataset = rb_ary_new2(numRows);
434
- for (i = 0; i < numRows; i++) {
435
- VALUE row = rb_mysql_result_fetch_row(argc, argv, self);
453
+ unsigned long rowsProcessed = 0;
454
+ rowsProcessed = RARRAY_LEN(wrapper->rows);
455
+ for (i = 0; i < wrapper->numberOfRows; i++) {
456
+ VALUE row;
457
+ if (i < rowsProcessed) {
458
+ row = rb_ary_entry(wrapper->rows, i);
459
+ } else {
460
+ row = rb_mysql_result_fetch_row(argc, argv, self);
461
+ rb_ary_store(wrapper->rows, i, row);
462
+ wrapper->lastRowProcessed++;
463
+ }
464
+
436
465
  if (row == Qnil) {
466
+ // we don't need the mysql C dataset around anymore, peace it
467
+ rb_mysql_result_free(wrapper->result);
437
468
  return Qnil;
438
469
  }
439
- rb_ary_store(dataset, i, row);
470
+
471
+ if (block != Qnil) {
472
+ rb_yield(row);
473
+ }
474
+ }
475
+ if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
476
+ // we don't need the mysql C dataset around anymore, peace it
477
+ rb_mysql_result_free(wrapper);
440
478
  }
441
- return dataset;
442
479
  }
443
- return Qnil;
480
+
481
+ return wrapper->rows;
444
482
  }
445
483
 
446
484
  /* Ruby Extension initializer */
@@ -43,9 +43,19 @@ static VALUE rb_mysql_client_affected_rows(VALUE self);
43
43
  void rb_mysql_client_free(void * client);
44
44
 
45
45
  /* Mysql2::Result */
46
- #define GetMysql2Result(obj, sval) (sval = (MYSQL_RES*)DATA_PTR(obj));
46
+ typedef struct {
47
+ VALUE fields;
48
+ VALUE rows;
49
+ unsigned long numberOfFields;
50
+ unsigned long numberOfRows;
51
+ unsigned long lastRowProcessed;
52
+ int resultFreed;
53
+ MYSQL_RES *result;
54
+ } mysql2_result_wrapper;
55
+ #define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj));
47
56
  VALUE cMysql2Result;
48
57
  static VALUE rb_mysql_result_to_obj(MYSQL_RES * res);
49
58
  static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self);
50
59
  static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self);
51
- void rb_mysql_result_free(void * result);
60
+ void rb_mysql_result_free(void * wrapper);
61
+ void rb_mysql_result_mark(void * wrapper);
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'mysql2' unless defined? Mysql2
4
- require 'active_record/connection_adapters/mysql_adapter'
5
4
 
6
5
  module ActiveRecord
7
6
  class Base
@@ -13,7 +12,26 @@ module ActiveRecord
13
12
  end
14
13
 
15
14
  module ConnectionAdapters
16
- class Mysql2Column < MysqlColumn
15
+ class Mysql2Column < Column
16
+ def extract_default(default)
17
+ if sql_type =~ /blob/i || type == :text
18
+ if default.blank?
19
+ return null ? nil : ''
20
+ else
21
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
22
+ end
23
+ elsif missing_default_forged_as_empty_string?(default)
24
+ nil
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def has_default?
31
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
32
+ super
33
+ end
34
+
17
35
  # Returns the Ruby class that corresponds to the abstract data type.
18
36
  def klass
19
37
  case type
@@ -37,25 +55,154 @@ module ActiveRecord
37
55
  value
38
56
  end
39
57
  end
40
-
58
+
41
59
  def type_cast_code(var_name)
42
60
  nil
43
61
  end
62
+
63
+ private
64
+ def simplified_type(field_type)
65
+ return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
66
+ return :string if field_type =~ /enum/i
67
+ super
68
+ end
69
+
70
+ def extract_limit(sql_type)
71
+ case sql_type
72
+ when /blob|text/i
73
+ case sql_type
74
+ when /tiny/i
75
+ 255
76
+ when /medium/i
77
+ 16777215
78
+ when /long/i
79
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
80
+ else
81
+ super # we could return 65535 here, but we leave it undecorated by default
82
+ end
83
+ when /^bigint/i; 8
84
+ when /^int/i; 4
85
+ when /^mediumint/i; 3
86
+ when /^smallint/i; 2
87
+ when /^tinyint/i; 1
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ # MySQL misreports NOT NULL column default when none is given.
94
+ # We can't detect this for columns which may have a legitimate ''
95
+ # default (string) but we can for others (integer, datetime, boolean,
96
+ # and the rest).
97
+ #
98
+ # Test whether the column has default '', is not null, and is not
99
+ # a type allowing default ''.
100
+ def missing_default_forged_as_empty_string?(default)
101
+ type != :string && !null && default == ''
102
+ end
44
103
  end
45
104
 
46
- class Mysql2Adapter < MysqlAdapter
47
- PRIMARY = "PRIMARY".freeze
48
- ADAPTER_NAME = "Mysql2".freeze
105
+ class Mysql2Adapter < AbstractAdapter
106
+ cattr_accessor :emulate_booleans
107
+ self.emulate_booleans = true
108
+
109
+ ADAPTER_NAME = 'MySQL'.freeze
110
+
111
+ LOST_CONNECTION_ERROR_MESSAGES = [
112
+ "Server shutdown in progress",
113
+ "Broken pipe",
114
+ "Lost connection to MySQL server during query",
115
+ "MySQL server has gone away" ]
116
+
117
+ QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
118
+
119
+ NATIVE_DATABASE_TYPES = {
120
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
121
+ :string => { :name => "varchar", :limit => 255 },
122
+ :text => { :name => "text" },
123
+ :integer => { :name => "int", :limit => 4 },
124
+ :float => { :name => "float" },
125
+ :decimal => { :name => "decimal" },
126
+ :datetime => { :name => "datetime" },
127
+ :timestamp => { :name => "datetime" },
128
+ :time => { :name => "time" },
129
+ :date => { :name => "date" },
130
+ :binary => { :name => "blob" },
131
+ :boolean => { :name => "tinyint", :limit => 1 }
132
+ }
133
+
134
+ def initialize(connection, logger, connection_options, config)
135
+ super(connection, logger)
136
+ @connection_options, @config = connection_options, config
137
+ @quoted_column_names, @quoted_table_names = {}, {}
138
+ end
49
139
 
50
140
  def adapter_name
51
141
  ADAPTER_NAME
52
142
  end
53
143
 
144
+ def supports_migrations?
145
+ true
146
+ end
147
+
148
+ def supports_primary_key?
149
+ true
150
+ end
151
+
152
+ def supports_savepoints?
153
+ true
154
+ end
155
+
156
+ def native_database_types
157
+ NATIVE_DATABASE_TYPES
158
+ end
159
+
54
160
  # QUOTING ==================================================
161
+
162
+ def quote(value, column = nil)
163
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
164
+ s = column.class.string_to_binary(value).unpack("H*")[0]
165
+ "x'#{s}'"
166
+ elsif value.kind_of?(BigDecimal)
167
+ value.to_s("F")
168
+ else
169
+ super
170
+ end
171
+ end
172
+
173
+ def quote_column_name(name) #:nodoc:
174
+ @quoted_column_names[name] ||= "`#{name}`"
175
+ end
176
+
177
+ def quote_table_name(name) #:nodoc:
178
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
179
+ end
180
+
55
181
  def quote_string(string)
56
182
  @connection.escape(string)
57
183
  end
58
184
 
185
+ def quoted_true
186
+ QUOTED_TRUE
187
+ end
188
+
189
+ def quoted_false
190
+ QUOTED_FALSE
191
+ end
192
+
193
+ # REFERENTIAL INTEGRITY ====================================
194
+
195
+ def disable_referential_integrity(&block) #:nodoc:
196
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
197
+
198
+ begin
199
+ update("SET FOREIGN_KEY_CHECKS = 0")
200
+ yield
201
+ ensure
202
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
203
+ end
204
+ end
205
+
59
206
  # CONNECTION MANAGEMENT ====================================
60
207
 
61
208
  def active?
@@ -88,14 +235,127 @@ module ActiveRecord
88
235
  select(sql, name)
89
236
  end
90
237
 
238
+ # Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it.
239
+ def execute(sql, name = nil)
240
+ if name == :skip_logging
241
+ @connection.query(sql)
242
+ else
243
+ log(sql, name) { @connection.query(sql) }
244
+ end
245
+ rescue ActiveRecord::StatementInvalid => exception
246
+ if exception.message.split(":").first =~ /Packets out of order/
247
+ 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."
248
+ else
249
+ raise
250
+ end
251
+ end
252
+
91
253
  def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
92
254
  super sql, name
93
255
  id_value || @connection.last_id
94
256
  end
95
257
  alias :create :insert_sql
96
258
 
259
+ def update_sql(sql, name = nil)
260
+ super
261
+ @connection.affected_rows
262
+ end
263
+
264
+ def begin_db_transaction
265
+ execute "BEGIN"
266
+ rescue Exception
267
+ # Transactions aren't supported
268
+ end
269
+
270
+ def commit_db_transaction
271
+ execute "COMMIT"
272
+ rescue Exception
273
+ # Transactions aren't supported
274
+ end
275
+
276
+ def rollback_db_transaction
277
+ execute "ROLLBACK"
278
+ rescue Exception
279
+ # Transactions aren't supported
280
+ end
281
+
282
+ def create_savepoint
283
+ execute("SAVEPOINT #{current_savepoint_name}")
284
+ end
285
+
286
+ def rollback_to_savepoint
287
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
288
+ end
289
+
290
+ def release_savepoint
291
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
292
+ end
293
+
294
+ def add_limit_offset!(sql, options)
295
+ limit, offset = options[:limit], options[:offset]
296
+ if limit && offset
297
+ sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
298
+ elsif limit
299
+ sql << " LIMIT #{sanitize_limit(limit)}"
300
+ elsif offset
301
+ sql << " OFFSET #{offset.to_i}"
302
+ end
303
+ sql
304
+ end
305
+
97
306
  # SCHEMA STATEMENTS ========================================
98
307
 
308
+ def structure_dump
309
+ if supports_views?
310
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
311
+ else
312
+ sql = "SHOW TABLES"
313
+ end
314
+
315
+ select_all(sql).inject("") do |structure, table|
316
+ table.delete('Table_type')
317
+ structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
318
+ end
319
+ end
320
+
321
+ def recreate_database(name, options = {})
322
+ drop_database(name)
323
+ create_database(name, options)
324
+ end
325
+
326
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
327
+ # Charset defaults to utf8.
328
+ #
329
+ # Example:
330
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
331
+ # create_database 'matt_development'
332
+ # create_database 'matt_development', :charset => :big5
333
+ def create_database(name, options = {})
334
+ if options[:collation]
335
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
336
+ else
337
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
338
+ end
339
+ end
340
+
341
+ def drop_database(name) #:nodoc:
342
+ execute "DROP DATABASE IF EXISTS `#{name}`"
343
+ end
344
+
345
+ def current_database
346
+ select_value 'SELECT DATABASE() as db'
347
+ end
348
+
349
+ # Returns the database character set.
350
+ def charset
351
+ show_variable 'character_set_database'
352
+ end
353
+
354
+ # Returns the database collation strategy.
355
+ def collation
356
+ show_variable 'collation_database'
357
+ end
358
+
99
359
  def tables(name = nil)
100
360
  tables = []
101
361
  execute("SHOW TABLES", name).each(:symbolize_keys => true) do |field|
@@ -104,6 +364,10 @@ module ActiveRecord
104
364
  tables
105
365
  end
106
366
 
367
+ def drop_table(table_name, options = {})
368
+ super(table_name, options)
369
+ end
370
+
107
371
  def indexes(table_name, name = nil)
108
372
  indexes = []
109
373
  current_index = nil
@@ -130,6 +394,89 @@ module ActiveRecord
130
394
  columns
131
395
  end
132
396
 
397
+ def create_table(table_name, options = {})
398
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
399
+ end
400
+
401
+ def rename_table(table_name, new_name)
402
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
403
+ end
404
+
405
+ def add_column(table_name, column_name, type, options = {})
406
+ 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])}"
407
+ add_column_options!(add_column_sql, options)
408
+ add_column_position!(add_column_sql, options)
409
+ execute(add_column_sql)
410
+ end
411
+
412
+ def change_column_default(table_name, column_name, default)
413
+ column = column_for(table_name, column_name)
414
+ change_column table_name, column_name, column.sql_type, :default => default
415
+ end
416
+
417
+ def change_column_null(table_name, column_name, null, default = nil)
418
+ column = column_for(table_name, column_name)
419
+
420
+ unless null || default.nil?
421
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
422
+ end
423
+
424
+ change_column table_name, column_name, column.sql_type, :null => null
425
+ end
426
+
427
+ def change_column(table_name, column_name, type, options = {})
428
+ column = column_for(table_name, column_name)
429
+
430
+ unless options_include_default?(options)
431
+ options[:default] = column.default
432
+ end
433
+
434
+ unless options.has_key?(:null)
435
+ options[:null] = column.null
436
+ end
437
+
438
+ 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])}"
439
+ add_column_options!(change_column_sql, options)
440
+ add_column_position!(change_column_sql, options)
441
+ execute(change_column_sql)
442
+ end
443
+
444
+ def rename_column(table_name, column_name, new_column_name)
445
+ options = {}
446
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
447
+ options[:default] = column.default
448
+ options[:null] = column.null
449
+ else
450
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
451
+ end
452
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
453
+ rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
454
+ add_column_options!(rename_column_sql, options)
455
+ execute(rename_column_sql)
456
+ end
457
+
458
+ # Maps logical Rails types to MySQL-specific data types.
459
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
460
+ return super unless type.to_s == 'integer'
461
+
462
+ case limit
463
+ when 1; 'tinyint'
464
+ when 2; 'smallint'
465
+ when 3; 'mediumint'
466
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
467
+ when 5..8; 'bigint'
468
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
469
+ end
470
+ end
471
+
472
+ def add_column_position!(sql, options)
473
+ if options[:first]
474
+ sql << " FIRST"
475
+ elsif options[:after]
476
+ sql << " AFTER #{quote_column_name(options[:after])}"
477
+ end
478
+ end
479
+
133
480
  def show_variable(name)
134
481
  variables = select_all("SHOW VARIABLES LIKE '#{name}'")
135
482
  variables.first[:Value] unless variables.empty?
@@ -138,17 +485,55 @@ module ActiveRecord
138
485
  def pk_and_sequence_for(table)
139
486
  keys = []
140
487
  result = execute("describe #{quote_table_name(table)}")
141
- result.each(:symbolize_keys) do |row|
488
+ result.each(:symbolize_keys => true) do |row|
142
489
  keys << row[:Field] if row[:Key] == "PRI"
143
490
  end
144
491
  keys.length == 1 ? [keys.first, nil] : nil
145
492
  end
146
493
 
494
+ # Returns just a table's primary key
495
+ def primary_key(table)
496
+ pk_and_sequence = pk_and_sequence_for(table)
497
+ pk_and_sequence && pk_and_sequence.first
498
+ end
499
+
500
+ def case_sensitive_equality_operator
501
+ "= BINARY"
502
+ end
503
+
504
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
505
+ where_sql
506
+ end
507
+
508
+ protected
509
+ # TODO: implement error_number method on Mysql2::Exception
510
+ def translate_exception(exception, message)
511
+ return super unless exception.respond_to?(:error_number)
512
+
513
+ case exception.error_number
514
+ when 1062
515
+ RecordNotUnique.new(message, exception)
516
+ when 1452
517
+ InvalidForeignKey.new(message, exception)
518
+ else
519
+ super
520
+ end
521
+ end
522
+
147
523
  private
148
524
  def connect
149
525
  # no-op
150
526
  end
151
527
 
528
+ def configure_connection
529
+ encoding = @config[:encoding]
530
+ execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
531
+
532
+ # By default, MySQL 'where id is null' selects the last inserted id.
533
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
534
+ execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
535
+ end
536
+
152
537
  def select(sql, name = nil)
153
538
  execute(sql, name).to_a
154
539
  end
@@ -160,6 +545,13 @@ module ActiveRecord
160
545
  def version
161
546
  @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
162
547
  end
548
+
549
+ def column_for(table_name, column_name)
550
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
551
+ raise "No such column: #{table_name}.#{column_name}"
552
+ end
553
+ column
554
+ end
163
555
  end
164
556
  end
165
557
  end
@@ -5,5 +5,5 @@ require 'mysql2_ext'
5
5
  #
6
6
  # A modern, simple and very fast Mysql library for Ruby - binding to libmysql
7
7
  module Mysql2
8
- VERSION = "0.1.3"
8
+ VERSION = "0.1.4"
9
9
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{mysql2}
8
- s.version = "0.1.3"
8
+ s.version = "0.1.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Brian Lopez"]
12
- s.date = %q{2010-04-15}
12
+ s.date = %q{2010-04-23}
13
13
  s.email = %q{seniorlopez@gmail.com}
14
14
  s.extensions = ["ext/extconf.rb"]
15
15
  s.extra_rdoc_files = [
@@ -41,6 +41,10 @@ describe Mysql2::Result do
41
41
  row.keys.first.class.should eql(Symbol)
42
42
  end
43
43
  end
44
+
45
+ it "should cache previously yielded results" do
46
+ @result.first.should eql(@result.first)
47
+ end
44
48
  end
45
49
 
46
50
  context "row data type mapping" do
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 3
9
- version: 0.1.3
8
+ - 4
9
+ version: 0.1.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Brian Lopez
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-15 00:00:00 -07:00
17
+ date: 2010-04-23 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies: []
20
20