sequel 3.21.0 → 3.32.0

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.
Files changed (234) hide show
  1. data/CHANGELOG +539 -3
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +20 -6
  4. data/Rakefile +37 -19
  5. data/bin/sequel +1 -5
  6. data/doc/advanced_associations.rdoc +17 -0
  7. data/doc/association_basics.rdoc +293 -43
  8. data/doc/dataset_basics.rdoc +4 -4
  9. data/doc/dataset_filtering.rdoc +8 -0
  10. data/doc/mass_assignment.rdoc +54 -0
  11. data/doc/migration.rdoc +15 -538
  12. data/doc/model_hooks.rdoc +72 -27
  13. data/doc/opening_databases.rdoc +89 -50
  14. data/doc/prepared_statements.rdoc +40 -13
  15. data/doc/reflection.rdoc +8 -2
  16. data/doc/release_notes/3.22.0.txt +39 -0
  17. data/doc/release_notes/3.23.0.txt +172 -0
  18. data/doc/release_notes/3.24.0.txt +420 -0
  19. data/doc/release_notes/3.25.0.txt +88 -0
  20. data/doc/release_notes/3.26.0.txt +88 -0
  21. data/doc/release_notes/3.27.0.txt +82 -0
  22. data/doc/release_notes/3.28.0.txt +304 -0
  23. data/doc/release_notes/3.29.0.txt +459 -0
  24. data/doc/release_notes/3.30.0.txt +135 -0
  25. data/doc/release_notes/3.31.0.txt +146 -0
  26. data/doc/release_notes/3.32.0.txt +202 -0
  27. data/doc/schema_modification.rdoc +547 -0
  28. data/doc/sharding.rdoc +7 -1
  29. data/doc/testing.rdoc +129 -0
  30. data/doc/transactions.rdoc +137 -0
  31. data/doc/virtual_rows.rdoc +2 -2
  32. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  33. data/lib/sequel/adapters/ado.rb +40 -18
  34. data/lib/sequel/adapters/amalgalite.rb +17 -9
  35. data/lib/sequel/adapters/db2.rb +175 -86
  36. data/lib/sequel/adapters/dbi.rb +15 -15
  37. data/lib/sequel/adapters/do/mysql.rb +5 -7
  38. data/lib/sequel/adapters/do/postgres.rb +0 -5
  39. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  40. data/lib/sequel/adapters/do.rb +17 -36
  41. data/lib/sequel/adapters/firebird.rb +27 -208
  42. data/lib/sequel/adapters/ibmdb.rb +448 -0
  43. data/lib/sequel/adapters/informix.rb +6 -23
  44. data/lib/sequel/adapters/jdbc/as400.rb +16 -34
  45. data/lib/sequel/adapters/jdbc/db2.rb +52 -0
  46. data/lib/sequel/adapters/jdbc/derby.rb +260 -0
  47. data/lib/sequel/adapters/jdbc/firebird.rb +29 -0
  48. data/lib/sequel/adapters/jdbc/h2.rb +66 -30
  49. data/lib/sequel/adapters/jdbc/hsqldb.rb +177 -0
  50. data/lib/sequel/adapters/jdbc/informix.rb +26 -0
  51. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  52. data/lib/sequel/adapters/jdbc/mssql.rb +3 -33
  53. data/lib/sequel/adapters/jdbc/mysql.rb +11 -11
  54. data/lib/sequel/adapters/jdbc/oracle.rb +88 -25
  55. data/lib/sequel/adapters/jdbc/postgresql.rb +9 -13
  56. data/lib/sequel/adapters/jdbc/sqlite.rb +2 -6
  57. data/lib/sequel/adapters/jdbc/sqlserver.rb +71 -0
  58. data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
  59. data/lib/sequel/adapters/jdbc.rb +231 -78
  60. data/lib/sequel/adapters/mock.rb +339 -0
  61. data/lib/sequel/adapters/mysql.rb +85 -184
  62. data/lib/sequel/adapters/mysql2.rb +26 -25
  63. data/lib/sequel/adapters/odbc/db2.rb +17 -0
  64. data/lib/sequel/adapters/odbc/mssql.rb +1 -6
  65. data/lib/sequel/adapters/odbc.rb +28 -9
  66. data/lib/sequel/adapters/openbase.rb +3 -5
  67. data/lib/sequel/adapters/oracle.rb +355 -53
  68. data/lib/sequel/adapters/postgres.rb +194 -33
  69. data/lib/sequel/adapters/shared/access.rb +32 -9
  70. data/lib/sequel/adapters/shared/db2.rb +328 -0
  71. data/lib/sequel/adapters/shared/firebird.rb +221 -0
  72. data/lib/sequel/adapters/shared/informix.rb +53 -0
  73. data/lib/sequel/adapters/shared/mssql.rb +278 -107
  74. data/lib/sequel/adapters/shared/mysql.rb +203 -65
  75. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +149 -0
  76. data/lib/sequel/adapters/shared/oracle.rb +237 -46
  77. data/lib/sequel/adapters/shared/postgres.rb +214 -82
  78. data/lib/sequel/adapters/shared/progress.rb +5 -8
  79. data/lib/sequel/adapters/shared/sqlite.rb +200 -50
  80. data/lib/sequel/adapters/sqlite.rb +29 -21
  81. data/lib/sequel/adapters/swift/mysql.rb +12 -7
  82. data/lib/sequel/adapters/swift/postgres.rb +0 -5
  83. data/lib/sequel/adapters/swift/sqlite.rb +8 -6
  84. data/lib/sequel/adapters/swift.rb +5 -5
  85. data/lib/sequel/adapters/tinytds.rb +160 -19
  86. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +66 -0
  87. data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
  88. data/lib/sequel/ast_transformer.rb +190 -0
  89. data/lib/sequel/connection_pool/threaded.rb +3 -2
  90. data/lib/sequel/connection_pool.rb +1 -1
  91. data/lib/sequel/core.rb +68 -5
  92. data/lib/sequel/database/connecting.rb +5 -6
  93. data/lib/sequel/database/dataset.rb +4 -4
  94. data/lib/sequel/database/dataset_defaults.rb +59 -1
  95. data/lib/sequel/database/logging.rb +1 -1
  96. data/lib/sequel/database/misc.rb +104 -19
  97. data/lib/sequel/database/query.rb +163 -59
  98. data/lib/sequel/database/schema_generator.rb +17 -7
  99. data/lib/sequel/database/schema_methods.rb +59 -29
  100. data/lib/sequel/dataset/actions.rb +240 -76
  101. data/lib/sequel/dataset/features.rb +91 -8
  102. data/lib/sequel/dataset/graph.rb +38 -11
  103. data/lib/sequel/dataset/misc.rb +50 -23
  104. data/lib/sequel/dataset/mutation.rb +12 -7
  105. data/lib/sequel/dataset/prepared_statements.rb +59 -27
  106. data/lib/sequel/dataset/query.rb +236 -43
  107. data/lib/sequel/dataset/sql.rb +676 -267
  108. data/lib/sequel/dataset.rb +8 -0
  109. data/lib/sequel/exceptions.rb +4 -0
  110. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  111. data/lib/sequel/extensions/migration.rb +6 -4
  112. data/lib/sequel/extensions/named_timezones.rb +5 -0
  113. data/lib/sequel/extensions/query.rb +1 -1
  114. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  115. data/lib/sequel/extensions/to_dot.rb +95 -83
  116. data/lib/sequel/model/associations.rb +1081 -344
  117. data/lib/sequel/model/base.rb +420 -133
  118. data/lib/sequel/model/errors.rb +1 -1
  119. data/lib/sequel/model/exceptions.rb +5 -1
  120. data/lib/sequel/model.rb +20 -10
  121. data/lib/sequel/plugins/active_model.rb +13 -1
  122. data/lib/sequel/plugins/association_pks.rb +22 -4
  123. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  124. data/lib/sequel/plugins/composition.rb +43 -10
  125. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  126. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  127. data/lib/sequel/plugins/force_encoding.rb +6 -6
  128. data/lib/sequel/plugins/identity_map.rb +114 -7
  129. data/lib/sequel/plugins/many_through_many.rb +68 -7
  130. data/lib/sequel/plugins/nested_attributes.rb +65 -10
  131. data/lib/sequel/plugins/prepared_statements.rb +151 -0
  132. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  133. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  134. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  135. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  136. data/lib/sequel/plugins/serialization.rb +98 -49
  137. data/lib/sequel/plugins/serialization_modification_detection.rb +63 -0
  138. data/lib/sequel/plugins/sharding.rb +21 -54
  139. data/lib/sequel/plugins/single_table_inheritance.rb +3 -1
  140. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  141. data/lib/sequel/plugins/update_primary_key.rb +2 -2
  142. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  143. data/lib/sequel/sql.rb +109 -52
  144. data/lib/sequel/timezones.rb +57 -38
  145. data/lib/sequel/version.rb +1 -1
  146. data/spec/adapters/db2_spec.rb +146 -0
  147. data/spec/adapters/mssql_spec.rb +135 -30
  148. data/spec/adapters/mysql_spec.rb +244 -212
  149. data/spec/adapters/oracle_spec.rb +67 -77
  150. data/spec/adapters/postgres_spec.rb +267 -78
  151. data/spec/adapters/spec_helper.rb +6 -5
  152. data/spec/adapters/sqlite_spec.rb +35 -21
  153. data/spec/core/connection_pool_spec.rb +71 -92
  154. data/spec/core/core_sql_spec.rb +21 -30
  155. data/spec/core/database_spec.rb +789 -512
  156. data/spec/core/dataset_spec.rb +1176 -1061
  157. data/spec/core/expression_filters_spec.rb +159 -42
  158. data/spec/core/mock_adapter_spec.rb +419 -0
  159. data/spec/core/object_graph_spec.rb +86 -114
  160. data/spec/core/schema_generator_spec.rb +3 -3
  161. data/spec/core/schema_spec.rb +298 -38
  162. data/spec/core/spec_helper.rb +6 -48
  163. data/spec/extensions/active_model_spec.rb +13 -0
  164. data/spec/extensions/association_autoreloading_spec.rb +1 -10
  165. data/spec/extensions/association_dependencies_spec.rb +2 -12
  166. data/spec/extensions/association_pks_spec.rb +66 -32
  167. data/spec/extensions/caching_spec.rb +23 -50
  168. data/spec/extensions/class_table_inheritance_spec.rb +31 -83
  169. data/spec/extensions/columns_introspection_spec.rb +91 -0
  170. data/spec/extensions/composition_spec.rb +18 -13
  171. data/spec/extensions/dataset_associations_spec.rb +199 -0
  172. data/spec/extensions/defaults_setter_spec.rb +64 -0
  173. data/spec/extensions/hook_class_methods_spec.rb +65 -91
  174. data/spec/extensions/identity_map_spec.rb +114 -22
  175. data/spec/extensions/instance_filters_spec.rb +11 -21
  176. data/spec/extensions/instance_hooks_spec.rb +53 -0
  177. data/spec/extensions/json_serializer_spec.rb +4 -5
  178. data/spec/extensions/lazy_attributes_spec.rb +16 -20
  179. data/spec/extensions/list_spec.rb +17 -39
  180. data/spec/extensions/many_through_many_spec.rb +283 -250
  181. data/spec/extensions/migration_spec.rb +35 -32
  182. data/spec/extensions/named_timezones_spec.rb +23 -3
  183. data/spec/extensions/nested_attributes_spec.rb +97 -88
  184. data/spec/extensions/optimistic_locking_spec.rb +9 -20
  185. data/spec/extensions/prepared_statements_associations_spec.rb +111 -0
  186. data/spec/extensions/prepared_statements_safe_spec.rb +61 -0
  187. data/spec/extensions/prepared_statements_spec.rb +53 -0
  188. data/spec/extensions/prepared_statements_with_pk_spec.rb +31 -0
  189. data/spec/extensions/pretty_table_spec.rb +1 -6
  190. data/spec/extensions/query_spec.rb +6 -0
  191. data/spec/extensions/rcte_tree_spec.rb +41 -43
  192. data/spec/extensions/schema_dumper_spec.rb +5 -8
  193. data/spec/extensions/schema_spec.rb +13 -21
  194. data/spec/extensions/serialization_modification_detection_spec.rb +72 -0
  195. data/spec/extensions/serialization_spec.rb +81 -43
  196. data/spec/extensions/sharding_spec.rb +66 -140
  197. data/spec/extensions/single_table_inheritance_spec.rb +18 -29
  198. data/spec/extensions/spec_helper.rb +13 -61
  199. data/spec/extensions/sql_expr_spec.rb +20 -60
  200. data/spec/extensions/tactical_eager_loading_spec.rb +9 -19
  201. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  202. data/spec/extensions/timestamps_spec.rb +6 -6
  203. data/spec/extensions/to_dot_spec.rb +4 -7
  204. data/spec/extensions/touch_spec.rb +13 -14
  205. data/spec/extensions/tree_spec.rb +11 -26
  206. data/spec/extensions/typecast_on_load_spec.rb +1 -6
  207. data/spec/extensions/update_primary_key_spec.rb +30 -24
  208. data/spec/extensions/validation_class_methods_spec.rb +30 -51
  209. data/spec/extensions/validation_helpers_spec.rb +16 -35
  210. data/spec/extensions/xml_serializer_spec.rb +16 -4
  211. data/spec/integration/associations_test.rb +661 -21
  212. data/spec/integration/database_test.rb +4 -2
  213. data/spec/integration/dataset_test.rb +500 -67
  214. data/spec/integration/eager_loader_test.rb +19 -21
  215. data/spec/integration/model_test.rb +80 -1
  216. data/spec/integration/plugin_test.rb +367 -119
  217. data/spec/integration/prepared_statement_test.rb +222 -125
  218. data/spec/integration/schema_test.rb +217 -39
  219. data/spec/integration/spec_helper.rb +39 -30
  220. data/spec/integration/timezone_test.rb +38 -12
  221. data/spec/integration/transaction_test.rb +182 -5
  222. data/spec/integration/type_test.rb +17 -3
  223. data/spec/model/association_reflection_spec.rb +201 -7
  224. data/spec/model/associations_spec.rb +955 -662
  225. data/spec/model/base_spec.rb +275 -95
  226. data/spec/model/dataset_methods_spec.rb +7 -27
  227. data/spec/model/eager_loading_spec.rb +677 -676
  228. data/spec/model/hooks_spec.rb +349 -67
  229. data/spec/model/model_spec.rb +222 -168
  230. data/spec/model/plugins_spec.rb +24 -13
  231. data/spec/model/record_spec.rb +381 -218
  232. data/spec/model/spec_helper.rb +13 -71
  233. data/spec/model/validations_spec.rb +11 -0
  234. metadata +117 -73
@@ -1,5 +1,6 @@
1
1
  module Sequel
2
2
  Dataset::NON_SQL_OPTIONS << :insert_ignore
3
+ Dataset::NON_SQL_OPTIONS << :update_ignore
3
4
  Dataset::NON_SQL_OPTIONS << :on_duplicate_key_update
4
5
 
5
6
  module MySQL
@@ -83,13 +84,9 @@ module Sequel
83
84
  @server_version ||= (m[1].to_i * 10000) + (m[2].to_i * 100) + m[3].to_i
84
85
  end
85
86
 
86
- # Return an array of symbols specifying table names in the current database.
87
- #
88
- # Options:
89
- # * :server - Set the server to use
90
- def tables(opts={})
91
- m = output_identifier_meth
92
- metadata_dataset.with_sql('SHOW TABLES').server(opts[:server]).map{|r| m.call(r.values.first)}
87
+ # MySQL supports CREATE TABLE IF NOT EXISTS syntax.
88
+ def supports_create_table_if_not_exists?
89
+ true
93
90
  end
94
91
 
95
92
  # MySQL supports prepared transactions (two-phase commit) using XA
@@ -107,6 +104,14 @@ module Sequel
107
104
  true
108
105
  end
109
106
 
107
+ # Return an array of symbols specifying table names in the current database.
108
+ #
109
+ # Options:
110
+ # * :server - Set the server to use
111
+ def tables(opts={})
112
+ full_tables('BASE TABLE', opts)
113
+ end
114
+
110
115
  # Changes the database in use by issuing a USE statement. I would be
111
116
  # very careful if I used this.
112
117
  def use(db_name)
@@ -116,6 +121,14 @@ module Sequel
116
121
  self
117
122
  end
118
123
 
124
+ # Return an array of symbols specifying view names in the current database.
125
+ #
126
+ # Options:
127
+ # * :server - Set the server to use
128
+ def views(opts={})
129
+ full_tables('VIEW', opts)
130
+ end
131
+
119
132
  private
120
133
 
121
134
  # Use MySQL specific syntax for rename column, set column type, and
@@ -142,6 +155,18 @@ module Sequel
142
155
  "ALTER TABLE #{quote_schema_table(table)} CHANGE COLUMN #{quote_identifier(op[:name])} #{column_definition_sql(op.merge(opts))}"
143
156
  when :drop_index
144
157
  "#{drop_index_sql(table, op)} ON #{quote_schema_table(table)}"
158
+ when :drop_constraint
159
+ type = case op[:type]
160
+ when :primary_key
161
+ return "ALTER TABLE #{quote_schema_table(table)} DROP PRIMARY KEY"
162
+ when :foreign_key
163
+ 'FOREIGN KEY'
164
+ when :unique
165
+ 'INDEX'
166
+ else
167
+ raise(Error, "must specify constraint type via :type=>(:foreign_key|:primary_key|:unique) when dropping constraints on MySQL")
168
+ end
169
+ "ALTER TABLE #{quote_schema_table(table)} DROP #{type} #{quote_identifier(op[:name])}"
145
170
  else
146
171
  super(table, op)
147
172
  end
@@ -161,9 +186,9 @@ module Sequel
161
186
  # Use XA START to start a new prepared transaction if the :prepare
162
187
  # option is given.
163
188
  def begin_transaction(conn, opts={})
164
- if s = opts[:prepare]
189
+ if (s = opts[:prepare]) && (th = @transactions[conn])[:savepoint_level] == 0
165
190
  log_connection_execute(conn, "XA START #{literal(s)}")
166
- conn
191
+ th[:savepoint_level] += 1
167
192
  else
168
193
  super
169
194
  end
@@ -184,7 +209,7 @@ module Sequel
184
209
  # Prepare the XA transaction for a two-phase commit if the
185
210
  # :prepare option is given.
186
211
  def commit_transaction(conn, opts={})
187
- if s = opts[:prepare]
212
+ if (s = opts[:prepare]) && @transactions[conn][:savepoint_level] <= 1
188
213
  log_connection_execute(conn, "XA END #{literal(s)}")
189
214
  log_connection_execute(conn, "XA PREPARE #{literal(s)}")
190
215
  else
@@ -205,6 +230,12 @@ module Sequel
205
230
  "#{super}#{" ENGINE=#{engine}" if engine}#{" DEFAULT CHARSET=#{charset}" if charset}#{" DEFAULT COLLATE=#{collate}" if collate}"
206
231
  end
207
232
 
233
+ # Backbone of the tables and views support using SHOW FULL TABLES.
234
+ def full_tables(type, opts)
235
+ m = output_identifier_meth
236
+ metadata_dataset.with_sql('SHOW FULL TABLES').server(opts[:server]).map{|r| m.call(r.values.first) if r.delete(:Table_type) == type}.compact
237
+ end
238
+
208
239
  # MySQL folds unquoted identifiers to lowercase, so it shouldn't need to upcase identifiers on input.
209
240
  def identifier_input_method_default
210
241
  nil
@@ -232,7 +263,7 @@ module Sequel
232
263
 
233
264
  # Rollback the currently open XA transaction
234
265
  def rollback_transaction(conn, opts={})
235
- if s = opts[:prepare]
266
+ if (s = opts[:prepare]) && @transactions[conn][:savepoint_level] <= 1
236
267
  log_connection_execute(conn, "XA END #{literal(s)}")
237
268
  log_connection_execute(conn, "XA PREPARE #{literal(s)}")
238
269
  log_connection_execute(conn, "XA ROLLBACK #{literal(s)}")
@@ -248,8 +279,8 @@ module Sequel
248
279
 
249
280
  # Use the MySQL specific DESCRIBE syntax to get a table description.
250
281
  def schema_parse_table(table_name, opts)
251
- m = output_identifier_meth
252
- im = input_identifier_meth
282
+ m = output_identifier_meth(opts[:dataset])
283
+ im = input_identifier_meth(opts[:dataset])
253
284
  metadata_dataset.with_sql("DESCRIBE ?", SQL::Identifier.new(im.call(table_name))).map do |row|
254
285
  row[:auto_increment] = true if row.delete(:Extra).to_s =~ /auto_increment/io
255
286
  row[:allow_null] = row.delete(:Null) == 'YES'
@@ -281,13 +312,17 @@ module Sequel
281
312
  # MySQL has both datetime and timestamp classes, most people are going
282
313
  # to want datetime
283
314
  def type_literal_generic_datetime(column)
284
- :datetime
315
+ if column[:default] == Sequel::CURRENT_TIMESTAMP
316
+ :timestamp
317
+ else
318
+ :datetime
319
+ end
285
320
  end
286
321
 
287
322
  # MySQL has both datetime and timestamp classes, most people are going
288
323
  # to want datetime
289
324
  def type_literal_generic_time(column)
290
- column[:only_time] ? :time : :datetime
325
+ column[:only_time] ? :time : type_literal_generic_datetime(column)
291
326
  end
292
327
 
293
328
  # MySQL doesn't have a true boolean class, so it uses tinyint(1)
@@ -303,27 +338,71 @@ module Sequel
303
338
  COMMA_SEPARATOR = ', '.freeze
304
339
  FOR_SHARE = ' LOCK IN SHARE MODE'.freeze
305
340
  SQL_CALC_FOUND_ROWS = ' SQL_CALC_FOUND_ROWS'.freeze
306
- DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'from where order limit')
307
- INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'ignore into columns values on_duplicate_key_update')
308
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'distinct calc_found_rows columns from join where group having compounds order limit lock')
309
- UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'table set where order limit')
341
+ DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'delete from where order limit')
342
+ INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'insert ignore into columns values on_duplicate_key_update')
343
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select distinct calc_found_rows columns from join where group having compounds order limit lock')
344
+ UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'update ignore table set where order limit')
345
+ SPACE = Dataset::SPACE
346
+ PAREN_OPEN = Dataset::PAREN_OPEN
347
+ PAREN_CLOSE = Dataset::PAREN_CLOSE
348
+ NOT_SPACE = Dataset::NOT_SPACE
349
+ FROM = Dataset::FROM
350
+ INSERT = Dataset::INSERT
351
+ COMMA = Dataset::COMMA
352
+ LIMIT = Dataset::LIMIT
353
+ GROUP_BY = Dataset::GROUP_BY
354
+ REGEXP = 'REGEXP'.freeze
355
+ LIKE = 'LIKE'.freeze
356
+ BINARY = 'BINARY '.freeze
357
+ CONCAT = "CONCAT".freeze
358
+ CAST_BITCOMP_OPEN = "CAST(~".freeze
359
+ CAST_BITCOMP_CLOSE = " AS SIGNED INTEGER)".freeze
360
+ STRAIGHT_JOIN = 'STRAIGHT_JOIN'.freeze
361
+ NATURAL_LEFT_JOIN = 'NATURAL LEFT JOIN'.freeze
362
+ BACKTICK = '`'.freeze
363
+ EMPTY_COLUMNS = " ()".freeze
364
+ EMPTY_VALUES = " VALUES ()".freeze
365
+ IGNORE = " IGNORE".freeze
366
+ REPLACE = 'REPLACE'.freeze
367
+ ON_DUPLICATE_KEY_UPDATE = " ON DUPLICATE KEY UPDATE ".freeze
368
+ EQ_VALUES = '=VALUES('.freeze
369
+ EQ = '='.freeze
370
+ WITH_ROLLUP = ' WITH ROLLUP'.freeze
310
371
 
311
372
  # MySQL specific syntax for LIKE/REGEXP searches, as well as
312
373
  # string concatenation.
313
- def complex_expression_sql(op, args)
374
+ def complex_expression_sql_append(sql, op, args)
314
375
  case op
376
+ when :IN, :"NOT IN"
377
+ ds = args.at(1)
378
+ if ds.is_a?(Sequel::Dataset) && ds.opts[:limit]
379
+ super(sql, op, [args.at(0), ds.from_self])
380
+ else
381
+ super
382
+ end
315
383
  when :~, :'!~', :'~*', :'!~*', :LIKE, :'NOT LIKE', :ILIKE, :'NOT ILIKE'
316
- "(#{literal(args.at(0))} #{'NOT ' if [:'NOT LIKE', :'NOT ILIKE', :'!~', :'!~*'].include?(op)}#{[:~, :'!~', :'~*', :'!~*'].include?(op) ? 'REGEXP' : 'LIKE'} #{'BINARY ' if [:~, :'!~', :LIKE, :'NOT LIKE'].include?(op)}#{literal(args.at(1))})"
384
+ sql << PAREN_OPEN
385
+ literal_append(sql, args.at(0))
386
+ sql << SPACE
387
+ sql << 'NOT ' if [:'NOT LIKE', :'NOT ILIKE', :'!~', :'!~*'].include?(op)
388
+ sql << ([:~, :'!~', :'~*', :'!~*'].include?(op) ? REGEXP : LIKE)
389
+ sql << SPACE
390
+ sql << BINARY if [:~, :'!~', :LIKE, :'NOT LIKE'].include?(op)
391
+ literal_append(sql, args.at(1))
392
+ sql << PAREN_CLOSE
317
393
  when :'||'
318
394
  if args.length > 1
319
- "CONCAT(#{args.collect{|a| literal(a)}.join(', ')})"
395
+ sql << CONCAT
396
+ array_sql_append(sql, args)
320
397
  else
321
- literal(args.at(0))
398
+ literal_append(sql, args.at(0))
322
399
  end
323
400
  when :'B~'
324
- "CAST(~#{literal(args.at(0))} AS SIGNED INTEGER)"
401
+ sql << CAST_BITCOMP_OPEN
402
+ literal_append(sql, args.at(0))
403
+ sql << CAST_BITCOMP_CLOSE
325
404
  else
326
- super(op, args)
405
+ super
327
406
  end
328
407
  end
329
408
 
@@ -351,8 +430,9 @@ module Sequel
351
430
  end
352
431
 
353
432
  # MySQL specific full text search syntax.
354
- def full_text_sql(cols, term, opts = {})
355
- "MATCH #{literal(Array(cols))} AGAINST (#{literal(Array(term).join(' '))}#{" IN BOOLEAN MODE" if opts[:boolean]})"
433
+ def full_text_sql(cols, terms, opts = {})
434
+ terms = terms.join(' ') if terms.is_a?(Array)
435
+ SQL::PlaceholderLiteralString.new("MATCH ? AGAINST (?#{" IN BOOLEAN MODE" if opts[:boolean]})", [Array(cols), terms], true)
356
436
  end
357
437
 
358
438
  # MySQL allows HAVING clause on ungrouped datasets.
@@ -372,9 +452,12 @@ module Sequel
372
452
  # STRAIGHT_JOIN.
373
453
  def join_type_sql(join_type)
374
454
  case join_type
375
- when :straight then 'STRAIGHT_JOIN'
376
- when :natural_inner then 'NATURAL LEFT JOIN'
377
- else super
455
+ when :straight
456
+ STRAIGHT_JOIN
457
+ when :natural_inner
458
+ NATURAL_LEFT_JOIN
459
+ else
460
+ super
378
461
  end
379
462
  end
380
463
 
@@ -389,7 +472,7 @@ module Sequel
389
472
  def insert_ignore
390
473
  clone(:insert_ignore=>true)
391
474
  end
392
-
475
+
393
476
  # Sets up the insert methods to use ON DUPLICATE KEY UPDATE
394
477
  # If you pass no arguments, ALL fields will be
395
478
  # updated with the new values. If you pass the fields you
@@ -415,7 +498,9 @@ module Sequel
415
498
 
416
499
  # MySQL specific syntax for inserting multiple values at once.
417
500
  def multi_insert_sql(columns, values)
418
- [insert_sql(columns, LiteralString.new('VALUES ' + values.map {|r| literal(Array(r))}.join(COMMA_SEPARATOR)))]
501
+ sql = LiteralString.new('VALUES ')
502
+ expression_list_append(sql, values.map{|r| Array(r)})
503
+ [insert_sql(columns, sql)]
419
504
  end
420
505
 
421
506
  # MySQL uses the number of rows actually modified in the update,
@@ -425,8 +510,8 @@ module Sequel
425
510
  end
426
511
 
427
512
  # MySQL uses the nonstandard ` (backtick) for quoting identifiers.
428
- def quoted_identifier(c)
429
- "`#{c}`"
513
+ def quoted_identifier_append(sql, c)
514
+ sql << BACKTICK << c.to_s << BACKTICK
430
515
  end
431
516
 
432
517
  # MySQL specific syntax for REPLACE (aka UPSERT, or update if exists,
@@ -441,6 +526,11 @@ module Sequel
441
526
  true
442
527
  end
443
528
 
529
+ # MySQL supports GROUP BY WITH ROLLUP (but not CUBE)
530
+ def supports_group_rollup?
531
+ true
532
+ end
533
+
444
534
  # MySQL does not support INTERSECT or EXCEPT
445
535
  def supports_intersect_except?
446
536
  false
@@ -451,6 +541,12 @@ module Sequel
451
541
  true
452
542
  end
453
543
 
544
+ # MySQL's DISTINCT ON emulation using GROUP BY does not respect the
545
+ # queries ORDER BY clause.
546
+ def supports_ordered_distinct_on?
547
+ false
548
+ end
549
+
454
550
  # MySQL does support fractional timestamps in literal timestamps, but it
455
551
  # ignores them. Also, using them seems to cause problems on 1.9. Since
456
552
  # they are ignored anyway, not using them is probably best.
@@ -458,13 +554,16 @@ module Sequel
458
554
  false
459
555
  end
460
556
 
461
- protected
462
-
463
- # If this is an replace instead of an insert, use replace instead
464
- def _insert_sql
465
- @opts[:replace] ? clause_sql(:replace) : super
557
+ # Sets up the update methods to use UPDATE IGNORE.
558
+ # Useful if you have a unique key and want to just skip
559
+ # updating rows that violate the unique key restriction.
560
+ #
561
+ # dataset.update_ignore.update({:name => 'a', :value => 1})
562
+ # # UPDATE IGNORE tablename SET name = 'a', value = 1
563
+ def update_ignore
564
+ clone(:update_ignore=>true)
466
565
  end
467
-
566
+
468
567
  private
469
568
 
470
569
  # MySQL supports the ORDER BY and LIMIT clauses for DELETE statements
@@ -476,7 +575,10 @@ module Sequel
476
575
  # from, but include the others for the purposes of selecting rows.
477
576
  def delete_from_sql(sql)
478
577
  if joined_dataset?
479
- sql << " #{source_list(@opts[:from][0..0])} FROM #{source_list(@opts[:from])}"
578
+ sql << SPACE
579
+ source_list_append(sql, @opts[:from][0..0])
580
+ sql << FROM
581
+ source_list_append(sql, @opts[:from])
480
582
  select_join_sql(sql)
481
583
  else
482
584
  super
@@ -493,7 +595,7 @@ module Sequel
493
595
  def insert_columns_sql(sql)
494
596
  values = opts[:values]
495
597
  if values.is_a?(Array) && values.empty?
496
- sql << " ()"
598
+ sql << EMPTY_COLUMNS
497
599
  else
498
600
  super
499
601
  end
@@ -501,19 +603,62 @@ module Sequel
501
603
 
502
604
  # MySQL supports INSERT IGNORE INTO
503
605
  def insert_ignore_sql(sql)
504
- sql << " IGNORE" if opts[:insert_ignore]
606
+ sql << IGNORE if opts[:insert_ignore]
607
+ end
608
+
609
+ # MySQL supports UPDATE IGNORE
610
+ def update_ignore_sql(sql)
611
+ sql << IGNORE if opts[:update_ignore]
612
+ end
613
+
614
+ # If this is an replace instead of an insert, use replace instead
615
+ def insert_insert_sql(sql)
616
+ sql << (@opts[:replace] ? REPLACE : INSERT)
505
617
  end
506
618
 
507
619
  # MySQL supports INSERT ... ON DUPLICATE KEY UPDATE
508
620
  def insert_on_duplicate_key_update_sql(sql)
509
- sql << on_duplicate_key_update_sql if opts[:on_duplicate_key_update]
621
+ if update_cols = opts[:on_duplicate_key_update]
622
+ update_vals = nil
623
+
624
+ if update_cols.empty?
625
+ update_cols = columns
626
+ elsif update_cols.last.is_a?(Hash)
627
+ update_vals = update_cols.last
628
+ update_cols = update_cols[0..-2]
629
+ end
630
+
631
+ sql << ON_DUPLICATE_KEY_UPDATE
632
+ c = false
633
+ co = COMMA
634
+ values = EQ_VALUES
635
+ endp = PAREN_CLOSE
636
+ update_cols.each do |col|
637
+ sql << co if c
638
+ quote_identifier_append(sql, col)
639
+ sql << values
640
+ quote_identifier_append(sql, col)
641
+ sql << endp
642
+ c ||= true
643
+ end
644
+ if update_vals
645
+ eq = EQ
646
+ update_vals.map do |col,v|
647
+ sql << co if c
648
+ quote_identifier_append(sql, col)
649
+ sql << eq
650
+ literal_append(sql, v)
651
+ c ||= true
652
+ end
653
+ end
654
+ end
510
655
  end
511
656
 
512
657
  # MySQL doesn't use the standard DEFAULT VALUES for empty values.
513
658
  def insert_values_sql(sql)
514
659
  values = opts[:values]
515
660
  if values.is_a?(Array) && values.empty?
516
- sql << " VALUES ()"
661
+ sql << EMPTY_VALUES
517
662
  else
518
663
  super
519
664
  end
@@ -521,7 +666,10 @@ module Sequel
521
666
 
522
667
  # MySQL allows a LIMIT in DELETE and UPDATE statements.
523
668
  def limit_sql(sql)
524
- sql << " LIMIT #{@opts[:limit]}" if @opts[:limit]
669
+ if l = @opts[:limit]
670
+ sql << LIMIT
671
+ literal_append(sql, @opts[:limit])
672
+ end
525
673
  end
526
674
  alias delete_limit_sql limit_sql
527
675
  alias update_limit_sql limit_sql
@@ -536,30 +684,20 @@ module Sequel
536
684
  BOOL_TRUE
537
685
  end
538
686
 
539
- # MySQL specific syntax for ON DUPLICATE KEY UPDATE
540
- def on_duplicate_key_update_sql
541
- if update_cols = opts[:on_duplicate_key_update]
542
- update_vals = nil
543
-
544
- if update_cols.empty?
545
- update_cols = columns
546
- elsif update_cols.last.is_a?(Hash)
547
- update_vals = update_cols.last
548
- update_cols = update_cols[0..-2]
549
- end
550
-
551
- updating = update_cols.map{|c| "#{quote_identifier(c)}=VALUES(#{quote_identifier(c)})" }
552
- updating += update_vals.map{|c,v| "#{quote_identifier(c)}=#{literal(v)}" } if update_vals
553
-
554
- " ON DUPLICATE KEY UPDATE #{updating.join(COMMA_SEPARATOR)}"
555
- end
556
- end
557
-
558
687
  # MySQL does not support the SQL WITH clause for SELECT statements
559
688
  def select_clause_methods
560
689
  SELECT_CLAUSE_METHODS
561
690
  end
562
691
 
692
+ # MySQL supports ROLLUP via nonstandard SQL syntax
693
+ def select_group_sql(sql)
694
+ if group = @opts[:group]
695
+ sql << GROUP_BY
696
+ expression_list_append(sql, group)
697
+ sql << WITH_ROLLUP if @opts[:group_options] == :rollup
698
+ end
699
+ end
700
+
563
701
  # Support FOR SHARE locking when using the :share lock style.
564
702
  def select_lock_sql(sql)
565
703
  @opts[:lock] == :share ? (sql << FOR_SHARE) : super
@@ -0,0 +1,149 @@
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_append(sql, ds)
67
+ ps = ds.to_prepared_statement(:select).clone(:append_sql => sql)
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
+ # Raise a more obvious error if you attempt to call a unnamed prepared statement.
80
+ def call(*)
81
+ raise Error, "Cannot call prepared statement without a name" if prepared_statement_name.nil?
82
+ super
83
+ end
84
+
85
+ private
86
+
87
+ # Execute the prepared statement with the bind arguments instead of
88
+ # the given SQL.
89
+ def execute(sql, opts={}, &block)
90
+ super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
91
+ end
92
+
93
+ # Same as execute, explicit due to intricacies of alias and super.
94
+ def execute_dui(sql, opts={}, &block)
95
+ super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
96
+ end
97
+ end
98
+
99
+ # Methods for MySQL stored procedures using the native driver.
100
+ module StoredProcedureMethods
101
+ include Sequel::Dataset::StoredProcedureMethods
102
+
103
+ private
104
+
105
+ # Execute the database stored procedure with the stored arguments.
106
+ def execute(sql, opts={}, &block)
107
+ super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
108
+ end
109
+
110
+ # Same as execute, explicit due to intricacies of alias and super.
111
+ def execute_dui(sql, opts={}, &block)
112
+ super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
113
+ end
114
+ end
115
+
116
+ # MySQL is different in that it supports prepared statements but not bound
117
+ # variables outside of prepared statements. The default implementation
118
+ # breaks the use of subselects in prepared statements, so extend the
119
+ # temporary prepared statement that this creates with a module that
120
+ # fixes it.
121
+ def call(type, bind_arguments={}, *values, &block)
122
+ ps = to_prepared_statement(type, values)
123
+ ps.extend(CallableStatementMethods)
124
+ ps.call(bind_arguments, &block)
125
+ end
126
+
127
+ # Store the given type of prepared statement in the associated database
128
+ # with the given name.
129
+ def prepare(type, name=nil, *values)
130
+ ps = to_prepared_statement(type, values)
131
+ ps.extend(PreparedStatementMethods)
132
+ if name
133
+ ps.prepared_statement_name = name
134
+ db.prepared_statements[name] = ps
135
+ end
136
+ ps
137
+ end
138
+
139
+ private
140
+
141
+ # Extend the dataset with the MySQL stored procedure methods.
142
+ def prepare_extend_sproc(ds)
143
+ ds.extend(StoredProcedureMethods)
144
+ end
145
+
146
+ end
147
+ end
148
+ end
149
+ end