mysql2 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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