sequel 3.26.0 → 3.27.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/Rakefile +2 -3
- data/doc/mass_assignment.rdoc +54 -0
- data/doc/migration.rdoc +9 -533
- data/doc/prepared_statements.rdoc +8 -7
- data/doc/release_notes/3.27.0.txt +82 -0
- data/doc/schema_modification.rdoc +547 -0
- data/doc/testing.rdoc +64 -0
- data/lib/sequel/adapters/amalgalite.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +3 -1
- data/lib/sequel/adapters/jdbc/h2.rb +11 -5
- data/lib/sequel/adapters/mysql.rb +4 -122
- data/lib/sequel/adapters/mysql2.rb +4 -13
- data/lib/sequel/adapters/odbc.rb +4 -1
- data/lib/sequel/adapters/odbc/db2.rb +21 -0
- data/lib/sequel/adapters/shared/mysql.rb +12 -0
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
- data/lib/sequel/adapters/tinytds.rb +122 -3
- data/lib/sequel/core.rb +4 -3
- data/lib/sequel/database/misc.rb +7 -10
- data/lib/sequel/dataset/misc.rb +1 -1
- data/lib/sequel/dataset/sql.rb +7 -0
- data/lib/sequel/model/associations.rb +2 -2
- data/lib/sequel/model/base.rb +60 -10
- data/lib/sequel/plugins/prepared_statements_safe.rb +17 -7
- data/lib/sequel/sql.rb +5 -0
- data/lib/sequel/timezones.rb +12 -3
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mysql_spec.rb +25 -21
- data/spec/core/database_spec.rb +200 -0
- data/spec/core/dataset_spec.rb +6 -0
- data/spec/extensions/prepared_statements_safe_spec.rb +10 -0
- data/spec/extensions/schema_dumper_spec.rb +2 -2
- data/spec/integration/schema_test.rb +30 -1
- data/spec/integration/type_test.rb +10 -3
- data/spec/model/base_spec.rb +44 -0
- data/spec/model/model_spec.rb +14 -0
- data/spec/model/record_spec.rb +131 -12
- metadata +14 -4
data/doc/testing.rdoc
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= Testing with Sequel
|
2
|
+
|
3
|
+
Whether or not you use Sequel in your application, you are usually going to want to have tests that ensure that your code works. When you are using Sequel, it's helpful to integrate it into your testing framework, and it's generally best to run each test in its own transaction if possible. That keeps all tests isolated from each other, and it's simple as it handles all of the cleanup for you. Sequel doesn't ship with helpers for common libraries, as the exact code you need is often application-specific, but this page offers some examples that you can either use directly or build on.
|
4
|
+
|
5
|
+
== Transactional tests
|
6
|
+
|
7
|
+
These run each test in its own transaction, the recommended way to test.
|
8
|
+
|
9
|
+
=== RSpec 1
|
10
|
+
|
11
|
+
class Spec::Example::ExampleGroup
|
12
|
+
def execute(*args, &block)
|
13
|
+
x = nil
|
14
|
+
Sequel::Model.db.transaction{x = super(*args, &block); raise Sequel::Rollback}
|
15
|
+
x
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
=== RSpec 2
|
20
|
+
|
21
|
+
class Spec::Example::ExampleGroup
|
22
|
+
around do |example|
|
23
|
+
Sequel::Model.db.transaction{example.call; raise Sequel::Rollback}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
=== Test::Unit
|
28
|
+
|
29
|
+
# Must use this class as the base class for your tests
|
30
|
+
class SequelTestCase < Test::Unit::TestCase
|
31
|
+
def run(*args, &block)
|
32
|
+
Sequel::Model.db.transaction do
|
33
|
+
super
|
34
|
+
raise Sequel::Rollback
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
== Nontransactional tests
|
40
|
+
|
41
|
+
In some cases, it is not possible to use transactions. For example, if you are testing a web application that is running in a separate process, you don't have access to that process's database connections, so you can't run your examples in transactions. In that case, the best way to handle things is to cleanup after each test by deleting or truncating the database tables used in the test.
|
42
|
+
|
43
|
+
The order in which you delete/truncate the tables is important if you are using referential integrity in your database (which you probably should be doing). If you are using referential integrity, you need to make sure to delete in tables referencing other tables before the tables that are being referenced. For example, if you have an +albums+ table with an +artist_id+ field referencing the +artists+ table, you want to delete/truncate the +albums+ table before the +artists+ table. Note that if you have cyclic references in your database, you will probably need to write your own custom cleaning code.
|
44
|
+
|
45
|
+
=== RSpec
|
46
|
+
|
47
|
+
class Spec::Example::ExampleGroup
|
48
|
+
after do
|
49
|
+
[:table1, :table2].each{|x| Sequel::Model.db.from(x).truncate}
|
50
|
+
# or
|
51
|
+
[:table1, :table2].each{|x| Sequel::Model.db.from(x).delete}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
=== Test::Unit
|
56
|
+
|
57
|
+
# Must use this class as the base class for your tests
|
58
|
+
class SequelTestCase < Test::Unit::TestCase
|
59
|
+
def teardown
|
60
|
+
[:table1, :table2].each{|x| Sequel::Model.db.from(x).truncate}
|
61
|
+
# or
|
62
|
+
[:table1, :table2].each{|x| Sequel::Model.db.from(x).delete}
|
63
|
+
end
|
64
|
+
end
|
@@ -31,6 +31,10 @@ module Sequel
|
|
31
31
|
def datetime(s)
|
32
32
|
Sequel.database_to_application_timestamp(s)
|
33
33
|
end
|
34
|
+
|
35
|
+
def time(s)
|
36
|
+
Sequel.string_to_time(s)
|
37
|
+
end
|
34
38
|
|
35
39
|
# Don't raise an error if the value is a string and the declared
|
36
40
|
# type doesn't match a known type, just return the value.
|
data/lib/sequel/adapters/jdbc.rb
CHANGED
@@ -579,8 +579,10 @@ module Sequel
|
|
579
579
|
# Convert the type. Used for converting Java types to ruby types.
|
580
580
|
def convert_type(v)
|
581
581
|
case v
|
582
|
-
when Java::JavaSQL::Timestamp
|
582
|
+
when Java::JavaSQL::Timestamp
|
583
583
|
Sequel.database_to_application_timestamp(v.to_string)
|
584
|
+
when Java::JavaSQL::Time
|
585
|
+
Sequel.string_to_time(v.to_string)
|
584
586
|
when Java::JavaSQL::Date
|
585
587
|
Sequel.string_to_date(v.to_string)
|
586
588
|
when Java::JavaIo::BufferedReader
|
@@ -151,11 +151,7 @@ module Sequel
|
|
151
151
|
|
152
152
|
private
|
153
153
|
|
154
|
-
# H2
|
155
|
-
def literal_blob(v)
|
156
|
-
literal_string v.unpack("H*").first
|
157
|
-
end
|
158
|
-
|
154
|
+
# Handle H2 specific clobs as strings.
|
159
155
|
def convert_type(v)
|
160
156
|
case v
|
161
157
|
when Java::OrgH2Jdbc::JdbcClob
|
@@ -165,6 +161,16 @@ module Sequel
|
|
165
161
|
end
|
166
162
|
end
|
167
163
|
|
164
|
+
# H2 expects hexadecimal strings for blob values
|
165
|
+
def literal_blob(v)
|
166
|
+
literal_string v.unpack("H*").first
|
167
|
+
end
|
168
|
+
|
169
|
+
# H2 handles fractional seconds in timestamps, but not in times
|
170
|
+
def literal_sqltime(v)
|
171
|
+
v.strftime("'%H:%M:%S'")
|
172
|
+
end
|
173
|
+
|
168
174
|
def select_clause_methods
|
169
175
|
SELECT_CLAUSE_METHODS
|
170
176
|
end
|
@@ -5,7 +5,7 @@ rescue LoadError
|
|
5
5
|
end
|
6
6
|
raise(LoadError, "require 'mysql' did not define Mysql::CLIENT_MULTI_RESULTS!\n You are probably using the pure ruby mysql.rb driver,\n which Sequel does not support. You need to install\n the C based adapter, and make sure that the mysql.so\n file is loaded instead of the mysql.rb file.\n") unless defined?(Mysql::CLIENT_MULTI_RESULTS)
|
7
7
|
|
8
|
-
Sequel.require %w'shared/
|
8
|
+
Sequel.require %w'shared/mysql_prepared_statements', 'adapters'
|
9
9
|
|
10
10
|
module Sequel
|
11
11
|
# Module for holding all MySQL-related classes and modules for Sequel.
|
@@ -84,18 +84,13 @@ module Sequel
|
|
84
84
|
# Database class for MySQL databases used with Sequel.
|
85
85
|
class Database < Sequel::Database
|
86
86
|
include Sequel::MySQL::DatabaseMethods
|
87
|
+
include Sequel::MySQL::PreparedStatements::DatabaseMethods
|
87
88
|
|
88
89
|
# Mysql::Error messages that indicate the current connection should be disconnected
|
89
90
|
MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away|Lost connection to MySQL server during query)/
|
90
91
|
|
91
92
|
set_adapter_scheme :mysql
|
92
93
|
|
93
|
-
# Support stored procedures on MySQL
|
94
|
-
def call_sproc(name, opts={}, &block)
|
95
|
-
args = opts[:args] || []
|
96
|
-
execute("CALL #{name}#{args.empty? ? '()' : literal(args)}", opts.merge(:sproc=>false), &block)
|
97
|
-
end
|
98
|
-
|
99
94
|
# Connect to the database. In addition to the usual database options,
|
100
95
|
# the following options have effect:
|
101
96
|
#
|
@@ -161,10 +156,7 @@ module Sequel
|
|
161
156
|
|
162
157
|
sqls.each{|sql| log_yield(sql){conn.query(sql)}}
|
163
158
|
|
164
|
-
|
165
|
-
attr_accessor :prepared_statements
|
166
|
-
end
|
167
|
-
conn.prepared_statements = {}
|
159
|
+
add_prepared_statements_cache(conn)
|
168
160
|
conn
|
169
161
|
end
|
170
162
|
|
@@ -173,18 +165,6 @@ module Sequel
|
|
173
165
|
MySQL::Dataset.new(self, opts)
|
174
166
|
end
|
175
167
|
|
176
|
-
# Executes the given SQL using an available connection, yielding the
|
177
|
-
# connection if the block is given.
|
178
|
-
def execute(sql, opts={}, &block)
|
179
|
-
if opts[:sproc]
|
180
|
-
call_sproc(sql, opts, &block)
|
181
|
-
elsif sql.is_a?(Symbol)
|
182
|
-
execute_prepared_statement(sql, opts, &block)
|
183
|
-
else
|
184
|
-
synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
168
|
# Return the version of the MySQL server two which we are connecting.
|
189
169
|
def server_version(server=nil)
|
190
170
|
@server_version ||= (synchronize(server){|conn| conn.server_version if conn.respond_to?(:server_version)} || super)
|
@@ -268,27 +248,6 @@ module Sequel
|
|
268
248
|
nil
|
269
249
|
end
|
270
250
|
|
271
|
-
# Executes a prepared statement on an available connection. If the
|
272
|
-
# prepared statement already exists for the connection and has the same
|
273
|
-
# SQL, reuse it, otherwise, prepare the new statement. Because of the
|
274
|
-
# usual MySQL stupidity, we are forced to name arguments via separate
|
275
|
-
# SET queries. Use @sequel_arg_N (for N starting at 1) for these
|
276
|
-
# arguments.
|
277
|
-
def execute_prepared_statement(ps_name, opts, &block)
|
278
|
-
args = opts[:arguments]
|
279
|
-
ps = prepared_statements[ps_name]
|
280
|
-
sql = ps.prepared_sql
|
281
|
-
synchronize(opts[:server]) do |conn|
|
282
|
-
unless conn.prepared_statements[ps_name] == sql
|
283
|
-
conn.prepared_statements[ps_name] = sql
|
284
|
-
_execute(conn, "PREPARE #{ps_name} FROM '#{::Mysql.quote(sql)}'", opts)
|
285
|
-
end
|
286
|
-
i = 0
|
287
|
-
_execute(conn, "SET " + args.map {|arg| "@sequel_arg_#{i+=1} = #{literal(arg)}"}.join(", "), opts) unless args.empty?
|
288
|
-
_execute(conn, "EXECUTE #{ps_name}#{" USING #{(1..i).map{|j| "@sequel_arg_#{j}"}.join(', ')}" unless i == 0}", opts, &block)
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
251
|
# Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
|
293
252
|
def schema_column_type(db_type)
|
294
253
|
Sequel::MySQL.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
|
@@ -298,67 +257,7 @@ module Sequel
|
|
298
257
|
# Dataset class for MySQL datasets accessed via the native driver.
|
299
258
|
class Dataset < Sequel::Dataset
|
300
259
|
include Sequel::MySQL::DatasetMethods
|
301
|
-
include
|
302
|
-
|
303
|
-
# Methods to add to MySQL prepared statement calls without using a
|
304
|
-
# real database prepared statement and bound variables.
|
305
|
-
module CallableStatementMethods
|
306
|
-
# Extend given dataset with this module so subselects inside subselects in
|
307
|
-
# prepared statements work.
|
308
|
-
def subselect_sql(ds)
|
309
|
-
ps = ds.to_prepared_statement(:select)
|
310
|
-
ps.extend(CallableStatementMethods)
|
311
|
-
ps = ps.bind(@opts[:bind_vars]) if @opts[:bind_vars]
|
312
|
-
ps.prepared_args = prepared_args
|
313
|
-
ps.prepared_sql
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
# Methods for MySQL prepared statements using the native driver.
|
318
|
-
module PreparedStatementMethods
|
319
|
-
include Sequel::Dataset::UnnumberedArgumentMapper
|
320
|
-
|
321
|
-
private
|
322
|
-
|
323
|
-
# Execute the prepared statement with the bind arguments instead of
|
324
|
-
# the given SQL.
|
325
|
-
def execute(sql, opts={}, &block)
|
326
|
-
super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
|
327
|
-
end
|
328
|
-
|
329
|
-
# Same as execute, explicit due to intricacies of alias and super.
|
330
|
-
def execute_dui(sql, opts={}, &block)
|
331
|
-
super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
# Methods for MySQL stored procedures using the native driver.
|
336
|
-
module StoredProcedureMethods
|
337
|
-
include Sequel::Dataset::StoredProcedureMethods
|
338
|
-
|
339
|
-
private
|
340
|
-
|
341
|
-
# Execute the database stored procedure with the stored arguments.
|
342
|
-
def execute(sql, opts={}, &block)
|
343
|
-
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
344
|
-
end
|
345
|
-
|
346
|
-
# Same as execute, explicit due to intricacies of alias and super.
|
347
|
-
def execute_dui(sql, opts={}, &block)
|
348
|
-
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
# MySQL is different in that it supports prepared statements but not bound
|
353
|
-
# variables outside of prepared statements. The default implementation
|
354
|
-
# breaks the use of subselects in prepared statements, so extend the
|
355
|
-
# temporary prepared statement that this creates with a module that
|
356
|
-
# fixes it.
|
357
|
-
def call(type, bind_arguments={}, *values, &block)
|
358
|
-
ps = to_prepared_statement(type, values)
|
359
|
-
ps.extend(CallableStatementMethods)
|
360
|
-
ps.call(bind_arguments, &block)
|
361
|
-
end
|
260
|
+
include Sequel::MySQL::PreparedStatements::DatasetMethods
|
362
261
|
|
363
262
|
# Delete rows matching this dataset
|
364
263
|
def delete
|
@@ -401,18 +300,6 @@ module Sequel
|
|
401
300
|
execute_dui(insert_sql(*values)){|c| return c.insert_id}
|
402
301
|
end
|
403
302
|
|
404
|
-
# Store the given type of prepared statement in the associated database
|
405
|
-
# with the given name.
|
406
|
-
def prepare(type, name=nil, *values)
|
407
|
-
ps = to_prepared_statement(type, values)
|
408
|
-
ps.extend(PreparedStatementMethods)
|
409
|
-
if name
|
410
|
-
ps.prepared_statement_name = name
|
411
|
-
db.prepared_statements[name] = ps
|
412
|
-
end
|
413
|
-
ps
|
414
|
-
end
|
415
|
-
|
416
303
|
# Replace (update or insert) the matching row.
|
417
304
|
def replace(*args)
|
418
305
|
execute_dui(replace_sql(*args)){|c| return c.insert_id}
|
@@ -456,11 +343,6 @@ module Sequel
|
|
456
343
|
"'#{::Mysql.quote(v)}'"
|
457
344
|
end
|
458
345
|
|
459
|
-
# Extend the dataset with the MySQL stored procedure methods.
|
460
|
-
def prepare_extend_sproc(ds)
|
461
|
-
ds.extend(StoredProcedureMethods)
|
462
|
-
end
|
463
|
-
|
464
346
|
# Yield each row of the given result set r with columns cols
|
465
347
|
# as a hash with symbol keys
|
466
348
|
def yield_rows(r, cols)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'mysql2' unless defined? Mysql2
|
2
|
-
|
3
|
-
Sequel.require %w'shared/mysql', 'adapters'
|
2
|
+
Sequel.require %w'shared/mysql_prepared_statements', 'adapters'
|
4
3
|
|
5
4
|
module Sequel
|
6
5
|
# Module for holding all Mysql2-related classes and modules for Sequel.
|
@@ -8,6 +7,7 @@ module Sequel
|
|
8
7
|
# Database class for MySQL databases used with Sequel.
|
9
8
|
class Database < Sequel::Database
|
10
9
|
include Sequel::MySQL::DatabaseMethods
|
10
|
+
include Sequel::MySQL::PreparedStatements::DatabaseMethods
|
11
11
|
|
12
12
|
# Mysql::Error messages that indicate the current connection should be disconnected
|
13
13
|
MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away)/
|
@@ -21,7 +21,6 @@ module Sequel
|
|
21
21
|
# a filter for an autoincrement column equals NULL to return the last
|
22
22
|
# inserted row.
|
23
23
|
# * :charset - Same as :encoding (:encoding takes precendence)
|
24
|
-
# * :compress - Set to false to not compress results from the server
|
25
24
|
# * :config_default_group - The default group to read from the in
|
26
25
|
# the MySQL config file.
|
27
26
|
# * :config_local_infile - If provided, sets the Mysql::OPT_LOCAL_INFILE
|
@@ -57,6 +56,7 @@ module Sequel
|
|
57
56
|
|
58
57
|
sqls.each{|sql| log_yield(sql){conn.query(sql)}}
|
59
58
|
|
59
|
+
add_prepared_statements_cache(conn)
|
60
60
|
conn
|
61
61
|
end
|
62
62
|
|
@@ -65,16 +65,6 @@ module Sequel
|
|
65
65
|
Mysql2::Dataset.new(self, opts)
|
66
66
|
end
|
67
67
|
|
68
|
-
# Executes the given SQL using an available connection, yielding the
|
69
|
-
# connection if the block is given.
|
70
|
-
def execute(sql, opts={}, &block)
|
71
|
-
if opts[:sproc]
|
72
|
-
call_sproc(sql, opts, &block)
|
73
|
-
else
|
74
|
-
synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
68
|
# Return the version of the MySQL server two which we are connecting.
|
79
69
|
def server_version(server=nil)
|
80
70
|
@server_version ||= (synchronize(server){|conn| conn.server_info[:id]} || super)
|
@@ -132,6 +122,7 @@ module Sequel
|
|
132
122
|
# Dataset class for MySQL datasets accessed via the native driver.
|
133
123
|
class Dataset < Sequel::Dataset
|
134
124
|
include Sequel::MySQL::DatasetMethods
|
125
|
+
include Sequel::MySQL::PreparedStatements::DatasetMethods
|
135
126
|
|
136
127
|
# Delete rows matching this dataset
|
137
128
|
def delete
|
data/lib/sequel/adapters/odbc.rb
CHANGED
@@ -19,6 +19,9 @@ module Sequel
|
|
19
19
|
when 'progress'
|
20
20
|
Sequel.ts_require 'adapters/shared/progress'
|
21
21
|
extend Sequel::Progress::DatabaseMethods
|
22
|
+
when 'db2'
|
23
|
+
Sequel.ts_require 'adapters/odbc/db2'
|
24
|
+
extend Sequel::ODBC::DB2::DatabaseMethods
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
@@ -126,7 +129,7 @@ module Sequel
|
|
126
129
|
Sequel.database_to_application_timestamp([v.year, v.month, v.day, v.hour, v.minute, v.second])
|
127
130
|
when ::ODBC::Time
|
128
131
|
now = ::Time.now
|
129
|
-
Sequel.
|
132
|
+
Sequel::SQLTime.local(now.year, now.month, now.day, v.hour, v.minute, v.second)
|
130
133
|
when ::ODBC::Date
|
131
134
|
Date.new(v.year, v.month, v.day)
|
132
135
|
else
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sequel
|
2
|
+
module ODBC
|
3
|
+
# Database and Dataset instance methods for DB2 specific
|
4
|
+
# support via ODBC.
|
5
|
+
module DB2
|
6
|
+
module DatabaseMethods
|
7
|
+
def dataset(opts=nil)
|
8
|
+
Sequel::ODBC::DB2::Dataset.new(self, opts)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Dataset < ODBC::Dataset
|
13
|
+
def select_limit_sql(sql)
|
14
|
+
if l = @opts[:limit]
|
15
|
+
sql << " FETCH FIRST #{l == 1 ? 'ROW' : "#{literal(l)} ROWS"} ONLY"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -154,6 +154,18 @@ module Sequel
|
|
154
154
|
"ALTER TABLE #{quote_schema_table(table)} CHANGE COLUMN #{quote_identifier(op[:name])} #{column_definition_sql(op.merge(opts))}"
|
155
155
|
when :drop_index
|
156
156
|
"#{drop_index_sql(table, op)} ON #{quote_schema_table(table)}"
|
157
|
+
when :drop_constraint
|
158
|
+
type = case op[:type]
|
159
|
+
when :primary_key
|
160
|
+
return "ALTER TABLE #{quote_schema_table(table)} DROP PRIMARY KEY"
|
161
|
+
when :foreign_key
|
162
|
+
'FOREIGN KEY'
|
163
|
+
when :unique
|
164
|
+
'INDEX'
|
165
|
+
else
|
166
|
+
raise(Error, "must specify constraint type via :type=>(:foreign_key|:primary_key|:unique) when dropping constraints on MySQL")
|
167
|
+
end
|
168
|
+
"ALTER TABLE #{quote_schema_table(table)} DROP #{type} #{quote_identifier(op[:name])}"
|
157
169
|
else
|
158
170
|
super(table, op)
|
159
171
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
Sequel.require %w'shared/mysql utils/stored_procedures', 'adapters'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module MySQL
|
5
|
+
# This module is used by the mysql and mysql2 adapters to support
|
6
|
+
# prepared statements and stored procedures.
|
7
|
+
module PreparedStatements
|
8
|
+
module DatabaseMethods
|
9
|
+
# Support stored procedures on MySQL
|
10
|
+
def call_sproc(name, opts={}, &block)
|
11
|
+
args = opts[:args] || []
|
12
|
+
execute("CALL #{name}#{args.empty? ? '()' : literal(args)}", opts.merge(:sproc=>false), &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes the given SQL using an available connection, yielding the
|
16
|
+
# connection if the block is given.
|
17
|
+
def execute(sql, opts={}, &block)
|
18
|
+
if opts[:sproc]
|
19
|
+
call_sproc(sql, opts, &block)
|
20
|
+
elsif sql.is_a?(Symbol)
|
21
|
+
execute_prepared_statement(sql, opts, &block)
|
22
|
+
else
|
23
|
+
synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def add_prepared_statements_cache(conn)
|
30
|
+
class << conn
|
31
|
+
attr_accessor :prepared_statements
|
32
|
+
end
|
33
|
+
conn.prepared_statements = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Executes a prepared statement on an available connection. If the
|
37
|
+
# prepared statement already exists for the connection and has the same
|
38
|
+
# SQL, reuse it, otherwise, prepare the new statement. Because of the
|
39
|
+
# usual MySQL stupidity, we are forced to name arguments via separate
|
40
|
+
# SET queries. Use @sequel_arg_N (for N starting at 1) for these
|
41
|
+
# arguments.
|
42
|
+
def execute_prepared_statement(ps_name, opts, &block)
|
43
|
+
args = opts[:arguments]
|
44
|
+
ps = prepared_statements[ps_name]
|
45
|
+
sql = ps.prepared_sql
|
46
|
+
synchronize(opts[:server]) do |conn|
|
47
|
+
unless conn.prepared_statements[ps_name] == sql
|
48
|
+
conn.prepared_statements[ps_name] = sql
|
49
|
+
_execute(conn, "PREPARE #{ps_name} FROM #{literal(sql)}", opts)
|
50
|
+
end
|
51
|
+
i = 0
|
52
|
+
_execute(conn, "SET " + args.map {|arg| "@sequel_arg_#{i+=1} = #{literal(arg)}"}.join(", "), opts) unless args.empty?
|
53
|
+
_execute(conn, "EXECUTE #{ps_name}#{" USING #{(1..i).map{|j| "@sequel_arg_#{j}"}.join(', ')}" unless i == 0}", opts, &block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
module DatasetMethods
|
59
|
+
include Sequel::Dataset::StoredProcedures
|
60
|
+
|
61
|
+
# Methods to add to MySQL prepared statement calls without using a
|
62
|
+
# real database prepared statement and bound variables.
|
63
|
+
module CallableStatementMethods
|
64
|
+
# Extend given dataset with this module so subselects inside subselects in
|
65
|
+
# prepared statements work.
|
66
|
+
def subselect_sql(ds)
|
67
|
+
ps = ds.to_prepared_statement(:select)
|
68
|
+
ps.extend(CallableStatementMethods)
|
69
|
+
ps = ps.bind(@opts[:bind_vars]) if @opts[:bind_vars]
|
70
|
+
ps.prepared_args = prepared_args
|
71
|
+
ps.prepared_sql
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Methods for MySQL prepared statements using the native driver.
|
76
|
+
module PreparedStatementMethods
|
77
|
+
include Sequel::Dataset::UnnumberedArgumentMapper
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Execute the prepared statement with the bind arguments instead of
|
82
|
+
# the given SQL.
|
83
|
+
def execute(sql, opts={}, &block)
|
84
|
+
super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Same as execute, explicit due to intricacies of alias and super.
|
88
|
+
def execute_dui(sql, opts={}, &block)
|
89
|
+
super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Methods for MySQL stored procedures using the native driver.
|
94
|
+
module StoredProcedureMethods
|
95
|
+
include Sequel::Dataset::StoredProcedureMethods
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Execute the database stored procedure with the stored arguments.
|
100
|
+
def execute(sql, opts={}, &block)
|
101
|
+
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Same as execute, explicit due to intricacies of alias and super.
|
105
|
+
def execute_dui(sql, opts={}, &block)
|
106
|
+
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# MySQL is different in that it supports prepared statements but not bound
|
111
|
+
# variables outside of prepared statements. The default implementation
|
112
|
+
# breaks the use of subselects in prepared statements, so extend the
|
113
|
+
# temporary prepared statement that this creates with a module that
|
114
|
+
# fixes it.
|
115
|
+
def call(type, bind_arguments={}, *values, &block)
|
116
|
+
ps = to_prepared_statement(type, values)
|
117
|
+
ps.extend(CallableStatementMethods)
|
118
|
+
ps.call(bind_arguments, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Store the given type of prepared statement in the associated database
|
122
|
+
# with the given name.
|
123
|
+
def prepare(type, name=nil, *values)
|
124
|
+
ps = to_prepared_statement(type, values)
|
125
|
+
ps.extend(PreparedStatementMethods)
|
126
|
+
if name
|
127
|
+
ps.prepared_statement_name = name
|
128
|
+
db.prepared_statements[name] = ps
|
129
|
+
end
|
130
|
+
ps
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Extend the dataset with the MySQL stored procedure methods.
|
136
|
+
def prepare_extend_sproc(ds)
|
137
|
+
ds.extend(StoredProcedureMethods)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|