sequel 3.21.0 → 3.31.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 (225) hide show
  1. data/CHANGELOG +485 -3
  2. data/README.rdoc +20 -6
  3. data/Rakefile +37 -19
  4. data/bin/sequel +1 -5
  5. data/doc/association_basics.rdoc +249 -43
  6. data/doc/dataset_basics.rdoc +4 -4
  7. data/doc/dataset_filtering.rdoc +8 -0
  8. data/doc/mass_assignment.rdoc +54 -0
  9. data/doc/migration.rdoc +15 -538
  10. data/doc/model_hooks.rdoc +72 -27
  11. data/doc/opening_databases.rdoc +89 -50
  12. data/doc/prepared_statements.rdoc +40 -13
  13. data/doc/reflection.rdoc +8 -2
  14. data/doc/release_notes/3.22.0.txt +39 -0
  15. data/doc/release_notes/3.23.0.txt +172 -0
  16. data/doc/release_notes/3.24.0.txt +420 -0
  17. data/doc/release_notes/3.25.0.txt +88 -0
  18. data/doc/release_notes/3.26.0.txt +88 -0
  19. data/doc/release_notes/3.27.0.txt +82 -0
  20. data/doc/release_notes/3.28.0.txt +304 -0
  21. data/doc/release_notes/3.29.0.txt +459 -0
  22. data/doc/release_notes/3.30.0.txt +135 -0
  23. data/doc/release_notes/3.31.0.txt +146 -0
  24. data/doc/schema_modification.rdoc +547 -0
  25. data/doc/sharding.rdoc +7 -1
  26. data/doc/testing.rdoc +129 -0
  27. data/doc/transactions.rdoc +137 -0
  28. data/doc/virtual_rows.rdoc +2 -2
  29. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  30. data/lib/sequel/adapters/ado.rb +40 -18
  31. data/lib/sequel/adapters/amalgalite.rb +17 -9
  32. data/lib/sequel/adapters/db2.rb +175 -86
  33. data/lib/sequel/adapters/dbi.rb +15 -15
  34. data/lib/sequel/adapters/do/mysql.rb +5 -7
  35. data/lib/sequel/adapters/do/postgres.rb +0 -5
  36. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  37. data/lib/sequel/adapters/do.rb +17 -36
  38. data/lib/sequel/adapters/firebird.rb +27 -208
  39. data/lib/sequel/adapters/ibmdb.rb +448 -0
  40. data/lib/sequel/adapters/informix.rb +6 -23
  41. data/lib/sequel/adapters/jdbc/as400.rb +16 -34
  42. data/lib/sequel/adapters/jdbc/db2.rb +45 -0
  43. data/lib/sequel/adapters/jdbc/derby.rb +247 -0
  44. data/lib/sequel/adapters/jdbc/firebird.rb +29 -0
  45. data/lib/sequel/adapters/jdbc/h2.rb +56 -29
  46. data/lib/sequel/adapters/jdbc/hsqldb.rb +170 -0
  47. data/lib/sequel/adapters/jdbc/informix.rb +26 -0
  48. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  49. data/lib/sequel/adapters/jdbc/mssql.rb +3 -33
  50. data/lib/sequel/adapters/jdbc/mysql.rb +11 -11
  51. data/lib/sequel/adapters/jdbc/oracle.rb +81 -25
  52. data/lib/sequel/adapters/jdbc/postgresql.rb +9 -13
  53. data/lib/sequel/adapters/jdbc/sqlite.rb +2 -6
  54. data/lib/sequel/adapters/jdbc/sqlserver.rb +71 -0
  55. data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
  56. data/lib/sequel/adapters/jdbc.rb +231 -78
  57. data/lib/sequel/adapters/mock.rb +335 -0
  58. data/lib/sequel/adapters/mysql.rb +82 -184
  59. data/lib/sequel/adapters/mysql2.rb +26 -25
  60. data/lib/sequel/adapters/odbc/db2.rb +17 -0
  61. data/lib/sequel/adapters/odbc/mssql.rb +1 -6
  62. data/lib/sequel/adapters/odbc.rb +28 -9
  63. data/lib/sequel/adapters/openbase.rb +3 -5
  64. data/lib/sequel/adapters/oracle.rb +351 -53
  65. data/lib/sequel/adapters/postgres.rb +194 -33
  66. data/lib/sequel/adapters/shared/access.rb +32 -9
  67. data/lib/sequel/adapters/shared/db2.rb +321 -0
  68. data/lib/sequel/adapters/shared/firebird.rb +221 -0
  69. data/lib/sequel/adapters/shared/informix.rb +53 -0
  70. data/lib/sequel/adapters/shared/mssql.rb +231 -106
  71. data/lib/sequel/adapters/shared/mysql.rb +181 -63
  72. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +149 -0
  73. data/lib/sequel/adapters/shared/oracle.rb +231 -41
  74. data/lib/sequel/adapters/shared/postgres.rb +214 -82
  75. data/lib/sequel/adapters/shared/progress.rb +4 -7
  76. data/lib/sequel/adapters/shared/sqlite.rb +201 -57
  77. data/lib/sequel/adapters/sqlite.rb +29 -21
  78. data/lib/sequel/adapters/swift/mysql.rb +12 -7
  79. data/lib/sequel/adapters/swift/postgres.rb +0 -5
  80. data/lib/sequel/adapters/swift/sqlite.rb +8 -6
  81. data/lib/sequel/adapters/swift.rb +5 -5
  82. data/lib/sequel/adapters/tinytds.rb +160 -19
  83. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +66 -0
  84. data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
  85. data/lib/sequel/ast_transformer.rb +190 -0
  86. data/lib/sequel/connection_pool/threaded.rb +3 -2
  87. data/lib/sequel/connection_pool.rb +1 -1
  88. data/lib/sequel/core.rb +46 -5
  89. data/lib/sequel/database/connecting.rb +5 -6
  90. data/lib/sequel/database/dataset.rb +4 -4
  91. data/lib/sequel/database/dataset_defaults.rb +59 -1
  92. data/lib/sequel/database/logging.rb +1 -1
  93. data/lib/sequel/database/misc.rb +104 -19
  94. data/lib/sequel/database/query.rb +153 -58
  95. data/lib/sequel/database/schema_generator.rb +17 -7
  96. data/lib/sequel/database/schema_methods.rb +59 -29
  97. data/lib/sequel/dataset/actions.rb +231 -76
  98. data/lib/sequel/dataset/features.rb +91 -8
  99. data/lib/sequel/dataset/graph.rb +38 -11
  100. data/lib/sequel/dataset/misc.rb +50 -23
  101. data/lib/sequel/dataset/mutation.rb +5 -6
  102. data/lib/sequel/dataset/prepared_statements.rb +48 -27
  103. data/lib/sequel/dataset/query.rb +236 -43
  104. data/lib/sequel/dataset/sql.rb +665 -253
  105. data/lib/sequel/dataset.rb +8 -0
  106. data/lib/sequel/exceptions.rb +4 -0
  107. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  108. data/lib/sequel/extensions/migration.rb +6 -4
  109. data/lib/sequel/extensions/named_timezones.rb +5 -0
  110. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  111. data/lib/sequel/extensions/to_dot.rb +95 -83
  112. data/lib/sequel/model/associations.rb +1020 -337
  113. data/lib/sequel/model/base.rb +414 -132
  114. data/lib/sequel/model/errors.rb +1 -1
  115. data/lib/sequel/model/exceptions.rb +5 -1
  116. data/lib/sequel/model.rb +15 -8
  117. data/lib/sequel/plugins/association_pks.rb +22 -4
  118. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  119. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  120. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  121. data/lib/sequel/plugins/force_encoding.rb +6 -6
  122. data/lib/sequel/plugins/identity_map.rb +114 -7
  123. data/lib/sequel/plugins/many_through_many.rb +65 -7
  124. data/lib/sequel/plugins/prepared_statements.rb +151 -0
  125. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  126. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  127. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  128. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  129. data/lib/sequel/plugins/serialization.rb +88 -44
  130. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  131. data/lib/sequel/plugins/sharding.rb +21 -54
  132. data/lib/sequel/plugins/single_table_inheritance.rb +3 -1
  133. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  134. data/lib/sequel/plugins/update_primary_key.rb +2 -2
  135. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  136. data/lib/sequel/sql.rb +109 -52
  137. data/lib/sequel/timezones.rb +57 -38
  138. data/lib/sequel/version.rb +1 -1
  139. data/spec/adapters/db2_spec.rb +146 -0
  140. data/spec/adapters/mssql_spec.rb +111 -29
  141. data/spec/adapters/mysql_spec.rb +135 -123
  142. data/spec/adapters/oracle_spec.rb +49 -77
  143. data/spec/adapters/postgres_spec.rb +267 -78
  144. data/spec/adapters/spec_helper.rb +6 -5
  145. data/spec/adapters/sqlite_spec.rb +35 -21
  146. data/spec/core/connection_pool_spec.rb +71 -92
  147. data/spec/core/core_sql_spec.rb +21 -30
  148. data/spec/core/database_spec.rb +788 -512
  149. data/spec/core/dataset_spec.rb +1073 -1053
  150. data/spec/core/expression_filters_spec.rb +159 -42
  151. data/spec/core/mock_adapter_spec.rb +419 -0
  152. data/spec/core/object_graph_spec.rb +86 -114
  153. data/spec/core/schema_generator_spec.rb +3 -3
  154. data/spec/core/schema_spec.rb +298 -38
  155. data/spec/core/spec_helper.rb +6 -48
  156. data/spec/extensions/association_autoreloading_spec.rb +1 -10
  157. data/spec/extensions/association_dependencies_spec.rb +2 -12
  158. data/spec/extensions/association_pks_spec.rb +66 -32
  159. data/spec/extensions/caching_spec.rb +23 -50
  160. data/spec/extensions/class_table_inheritance_spec.rb +31 -83
  161. data/spec/extensions/columns_introspection_spec.rb +91 -0
  162. data/spec/extensions/composition_spec.rb +18 -13
  163. data/spec/extensions/dataset_associations_spec.rb +199 -0
  164. data/spec/extensions/defaults_setter_spec.rb +64 -0
  165. data/spec/extensions/hook_class_methods_spec.rb +65 -91
  166. data/spec/extensions/identity_map_spec.rb +114 -22
  167. data/spec/extensions/instance_filters_spec.rb +11 -21
  168. data/spec/extensions/instance_hooks_spec.rb +53 -0
  169. data/spec/extensions/json_serializer_spec.rb +4 -5
  170. data/spec/extensions/lazy_attributes_spec.rb +16 -20
  171. data/spec/extensions/list_spec.rb +17 -39
  172. data/spec/extensions/many_through_many_spec.rb +283 -250
  173. data/spec/extensions/migration_spec.rb +35 -32
  174. data/spec/extensions/named_timezones_spec.rb +23 -3
  175. data/spec/extensions/nested_attributes_spec.rb +97 -88
  176. data/spec/extensions/optimistic_locking_spec.rb +9 -20
  177. data/spec/extensions/prepared_statements_associations_spec.rb +111 -0
  178. data/spec/extensions/prepared_statements_safe_spec.rb +61 -0
  179. data/spec/extensions/prepared_statements_spec.rb +53 -0
  180. data/spec/extensions/prepared_statements_with_pk_spec.rb +31 -0
  181. data/spec/extensions/pretty_table_spec.rb +1 -6
  182. data/spec/extensions/rcte_tree_spec.rb +41 -43
  183. data/spec/extensions/schema_dumper_spec.rb +5 -8
  184. data/spec/extensions/schema_spec.rb +13 -21
  185. data/spec/extensions/serialization_modification_detection_spec.rb +37 -0
  186. data/spec/extensions/serialization_spec.rb +72 -43
  187. data/spec/extensions/sharding_spec.rb +66 -140
  188. data/spec/extensions/single_table_inheritance_spec.rb +18 -29
  189. data/spec/extensions/spec_helper.rb +13 -61
  190. data/spec/extensions/sql_expr_spec.rb +20 -60
  191. data/spec/extensions/tactical_eager_loading_spec.rb +9 -19
  192. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  193. data/spec/extensions/timestamps_spec.rb +6 -6
  194. data/spec/extensions/to_dot_spec.rb +4 -7
  195. data/spec/extensions/touch_spec.rb +13 -14
  196. data/spec/extensions/tree_spec.rb +11 -26
  197. data/spec/extensions/typecast_on_load_spec.rb +1 -6
  198. data/spec/extensions/update_primary_key_spec.rb +30 -24
  199. data/spec/extensions/validation_class_methods_spec.rb +30 -51
  200. data/spec/extensions/validation_helpers_spec.rb +16 -35
  201. data/spec/extensions/xml_serializer_spec.rb +16 -4
  202. data/spec/integration/associations_test.rb +537 -21
  203. data/spec/integration/database_test.rb +4 -2
  204. data/spec/integration/dataset_test.rb +461 -65
  205. data/spec/integration/eager_loader_test.rb +19 -21
  206. data/spec/integration/model_test.rb +80 -1
  207. data/spec/integration/plugin_test.rb +304 -116
  208. data/spec/integration/prepared_statement_test.rb +212 -122
  209. data/spec/integration/schema_test.rb +161 -30
  210. data/spec/integration/spec_helper.rb +39 -30
  211. data/spec/integration/timezone_test.rb +38 -12
  212. data/spec/integration/transaction_test.rb +172 -5
  213. data/spec/integration/type_test.rb +17 -3
  214. data/spec/model/association_reflection_spec.rb +201 -7
  215. data/spec/model/associations_spec.rb +865 -662
  216. data/spec/model/base_spec.rb +275 -95
  217. data/spec/model/dataset_methods_spec.rb +7 -27
  218. data/spec/model/eager_loading_spec.rb +677 -676
  219. data/spec/model/hooks_spec.rb +309 -67
  220. data/spec/model/model_spec.rb +222 -168
  221. data/spec/model/plugins_spec.rb +24 -13
  222. data/spec/model/record_spec.rb +381 -218
  223. data/spec/model/spec_helper.rb +13 -71
  224. data/spec/model/validations_spec.rb +11 -0
  225. metadata +115 -73
@@ -1,3 +1,5 @@
1
+ Sequel.require 'adapters/utils/emulate_offset_with_row_number'
2
+
1
3
  module Sequel
2
4
  Dataset::NON_SQL_OPTIONS << :disable_insert_output
3
5
  module MSSQL
@@ -11,10 +13,16 @@ module Sequel
11
13
  SQL_ROLLBACK_TO_SAVEPOINT = 'IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION autopoint_%d'.freeze
12
14
  SQL_SAVEPOINT = 'SAVE TRANSACTION autopoint_%d'.freeze
13
15
 
16
+ # Whether to use N'' to quote strings, which allows unicode characters inside the
17
+ # strings. True by default for compatibility, can be set to false for a possible
18
+ # performance increase. This sets the default for all datasets created from this
19
+ # Database object.
20
+ attr_accessor :mssql_unicode_strings
21
+
14
22
  # The types to check for 0 scale to transform :decimal types
15
23
  # to :integer.
16
24
  DECIMAL_TYPE_RE = /number|numeric|decimal/io
17
-
25
+
18
26
  # Microsoft SQL Server uses the :mssql type.
19
27
  def database_type
20
28
  :mssql
@@ -47,11 +55,13 @@ module Sequel
47
55
  # Microsoft SQL Server supports using the INFORMATION_SCHEMA to get
48
56
  # information on tables.
49
57
  def tables(opts={})
50
- m = output_identifier_meth
51
- metadata_dataset.from(:information_schema__tables___t).
52
- select(:table_name).
53
- filter(:table_type=>'BASE TABLE', :table_schema=>(opts[:schema]||default_schema||'dbo').to_s).
54
- map{|x| m.call(x[:table_name])}
58
+ information_schema_tables('BASE TABLE', opts)
59
+ end
60
+
61
+ # Microsoft SQL Server supports using the INFORMATION_SCHEMA to get
62
+ # information on views.
63
+ def views(opts={})
64
+ information_schema_tables('VIEW', opts)
55
65
  end
56
66
 
57
67
  private
@@ -97,7 +107,7 @@ module Sequel
97
107
  # Commit the active transaction on the connection, does not commit/release
98
108
  # savepoints.
99
109
  def commit_transaction(conn, opts={})
100
- log_connection_execute(conn, commit_transaction_sql) unless Thread.current[:sequel_transaction_depth] > 1
110
+ log_connection_execute(conn, commit_transaction_sql) unless @transactions[conn][:savepoint_level] > 1
101
111
  end
102
112
 
103
113
  # SQL to COMMIT a transaction.
@@ -117,6 +127,15 @@ module Sequel
117
127
  "DROP INDEX #{quote_identifier(op[:name] || default_index_name(table, op[:columns]))} ON #{quote_schema_table(table)}"
118
128
  end
119
129
 
130
+ # Backbone of the tables and views support.
131
+ def information_schema_tables(type, opts)
132
+ m = output_identifier_meth
133
+ metadata_dataset.from(:information_schema__tables___t).
134
+ select(:table_name).
135
+ filter(:table_type=>type, :table_schema=>(opts[:schema]||default_schema||'dbo').to_s).
136
+ map{|x| m.call(x[:table_name])}
137
+ end
138
+
120
139
  # Always quote identifiers in the metadata_dataset, so schema parsing works.
121
140
  def metadata_dataset
122
141
  ds = super
@@ -139,20 +158,41 @@ module Sequel
139
158
  SQL_ROLLBACK
140
159
  end
141
160
 
142
- # MSSQL uses the INFORMATION_SCHEMA to hold column information. This method does
143
- # not support the parsing of primary key information.
161
+ # The closest MSSQL equivalent of a boolean datatype is the bit type.
162
+ def schema_column_type(db_type)
163
+ case db_type
164
+ when /\A(bit)\z/io
165
+ :boolean
166
+ else
167
+ super
168
+ end
169
+ end
170
+
171
+ # MSSQL uses the INFORMATION_SCHEMA to hold column information, and
172
+ # parses primary key information from the sysindexes, sysindexkeys,
173
+ # and syscolumns system tables.
144
174
  def schema_parse_table(table_name, opts)
145
- m = output_identifier_meth
146
- m2 = input_identifier_meth
175
+ m = output_identifier_meth(opts[:dataset])
176
+ m2 = input_identifier_meth(opts[:dataset])
177
+ tn = m2.call(table_name.to_s)
178
+ table_id = get{object_id(tn)}
179
+ pk_index_id = metadata_dataset.from(:sysindexes).
180
+ where(:id=>table_id, :indid=>1..254){{(status & 2048)=>2048}}.
181
+ get(:indid)
182
+ pk_cols = metadata_dataset.from(:sysindexkeys___sik).
183
+ join(:syscolumns___sc, :id=>:id, :colid=>:colid).
184
+ where(:sik__id=>table_id, :sik__indid=>pk_index_id).
185
+ select_order_map(:sc__name)
147
186
  ds = metadata_dataset.from(:information_schema__tables___t).
148
187
  join(:information_schema__columns___c, :table_catalog=>:table_catalog,
149
188
  :table_schema => :table_schema, :table_name => :table_name).
150
189
  select(:column_name___column, :data_type___db_type, :character_maximum_length___max_chars, :column_default___default, :is_nullable___allow_null, :numeric_precision___column_size, :numeric_scale___scale).
151
- filter(:c__table_name=>m2.call(table_name.to_s))
190
+ filter(:c__table_name=>tn)
152
191
  if schema = opts[:schema] || default_schema
153
192
  ds.filter!(:c__table_schema=>schema)
154
193
  end
155
194
  ds.map do |row|
195
+ row[:primary_key] = pk_cols.include?(row[:column])
156
196
  row[:allow_null] = row[:allow_null] == 'YES' ? true : false
157
197
  row[:default] = nil if blank_object?(row[:default])
158
198
  row[:type] = if row[:db_type] =~ DECIMAL_TYPE_RE && row[:scale] == 0
@@ -163,6 +203,11 @@ module Sequel
163
203
  [m.call(row.delete(:column)), row]
164
204
  end
165
205
  end
206
+
207
+ # Set the mssql_unicode_strings settings from the given options.
208
+ def set_mssql_unicode_strings
209
+ @mssql_unicode_strings = typecast_value_boolean(@opts.fetch(:mssql_unicode_strings, true))
210
+ end
166
211
 
167
212
  # MSSQL has both datetime and timestamp classes, most people are going
168
213
  # to want datetime
@@ -189,49 +234,103 @@ module Sequel
189
234
  # support for clustered index type
190
235
  def index_definition_sql(table_name, index)
191
236
  index_name = index[:name] || default_index_name(table_name, index[:columns])
192
- clustered = index[:type] == :clustered
193
- if index[:where]
194
- raise Error, "Partial indexes are not supported for this database"
237
+ raise Error, "Partial indexes are not supported for this database" if index[:where]
238
+ if index[:type] == :full_text
239
+ "CREATE FULLTEXT INDEX ON #{quote_schema_table(table_name)} #{literal(index[:columns])} KEY INDEX #{literal(index[:key_index])}"
195
240
  else
196
- "CREATE #{'UNIQUE ' if index[:unique]}#{'CLUSTERED ' if clustered}INDEX #{quote_identifier(index_name)} ON #{quote_schema_table(table_name)} #{literal(index[:columns])}"
241
+ "CREATE #{'UNIQUE ' if index[:unique]}#{'CLUSTERED ' if index[:type] == :clustered}INDEX #{quote_identifier(index_name)} ON #{quote_schema_table(table_name)} #{literal(index[:columns])}"
197
242
  end
198
243
  end
199
244
  end
200
245
 
201
246
  module DatasetMethods
247
+ include EmulateOffsetWithRowNumber
248
+
202
249
  BOOL_TRUE = '1'.freeze
203
250
  BOOL_FALSE = '0'.freeze
204
251
  COMMA_SEPARATOR = ', '.freeze
205
- DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'with from output from2 where')
206
- INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with into columns output values')
207
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with distinct limit columns into from lock join where group having order compounds')
208
- UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'with table set output from where')
252
+ DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'with delete from output from2 where')
253
+ INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with insert into columns output values')
254
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with select distinct limit columns into from lock join where group having order compounds')
255
+ UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'with update table set output from where')
209
256
  NOLOCK = ' WITH (NOLOCK)'.freeze
210
257
  UPDLOCK = ' WITH (UPDLOCK)'.freeze
211
258
  WILDCARD = LiteralString.new('*').freeze
212
259
  CONSTANT_MAP = {:CURRENT_DATE=>'CAST(CURRENT_TIMESTAMP AS DATE)'.freeze, :CURRENT_TIME=>'CAST(CURRENT_TIMESTAMP AS TIME)'.freeze}
260
+ EXTRACT_MAP = {:year=>"yy", :month=>"m", :day=>"d", :hour=>"hh", :minute=>"n", :second=>"s"}
261
+ BRACKET_CLOSE = Dataset::BRACKET_CLOSE
262
+ BRACKET_OPEN = Dataset::BRACKET_OPEN
263
+ COMMA = Dataset::COMMA
264
+ PAREN_CLOSE = Dataset::PAREN_CLOSE
265
+ PAREN_SPACE_OPEN = Dataset::PAREN_SPACE_OPEN
266
+ SPACE = Dataset::SPACE
267
+ FROM = Dataset::FROM
268
+ APOS = Dataset::APOS
269
+ APOS_RE = Dataset::APOS_RE
270
+ DOUBLE_APOS = Dataset::DOUBLE_APOS
271
+ INTO = Dataset::INTO
272
+ DATEPART_SECOND_OPEN = "CAST((datepart(".freeze
273
+ DATEPART_SECOND_MIDDLE = ') + datepart(ns, '.freeze
274
+ DATEPART_SECOND_CLOSE = ")/1000000000.0) AS double precision)".freeze
275
+ DATEPART_OPEN = "datepart(".freeze
276
+ UNION_ALL = ' UNION ALL '.freeze
277
+ SELECT_SPACE = 'SELECT '.freeze
278
+ TIMESTAMP_USEC_FORMAT = ".%03d".freeze
279
+ OUTPUT_INSERTED = " OUTPUT INSERTED.*".freeze
280
+ HEX_START = '0x'.freeze
281
+ UNICODE_STRING_START = "N'".freeze
282
+ TOP_PAREN = " TOP (".freeze
283
+ TOP = " TOP ".freeze
284
+ OUTPUT = " OUTPUT ".freeze
285
+ HSTAR = "H*".freeze
286
+ CASE_SENSITIVE_COLLATION = 'Latin1_General_CS_AS'.freeze
287
+ CASE_INSENSITIVE_COLLATION = 'Latin1_General_CI_AS'.freeze
288
+
289
+ # Allow overriding of the mssql_unicode_strings option at the dataset level.
290
+ attr_accessor :mssql_unicode_strings
291
+
292
+ # Copy the mssql_unicode_strings option from the +db+ object.
293
+ def initialize(db, opts={})
294
+ super
295
+ @mssql_unicode_strings = db.mssql_unicode_strings
296
+ end
213
297
 
214
298
  # MSSQL uses + for string concatenation, and LIKE is case insensitive by default.
215
- def complex_expression_sql(op, args)
299
+ def complex_expression_sql_append(sql, op, args)
216
300
  case op
217
301
  when :'||'
218
- super(:+, args)
219
- when :ILIKE
220
- super(:LIKE, args)
221
- when :"NOT ILIKE"
222
- super(:"NOT LIKE", args)
302
+ super(sql, :+, args)
303
+ when :LIKE, :"NOT LIKE"
304
+ super(sql, op, args.map{|a| LiteralString.new("(#{literal(a)} COLLATE #{CASE_SENSITIVE_COLLATION})")})
305
+ when :ILIKE, :"NOT ILIKE"
306
+ super(sql, (op == :ILIKE ? :LIKE : :"NOT LIKE"), args.map{|a| LiteralString.new("(#{literal(a)} COLLATE #{CASE_INSENSITIVE_COLLATION})")})
223
307
  when :<<
224
- "(#{literal(args[0])} * POWER(2, #{literal(args[1])}))"
308
+ sql << complex_expression_arg_pairs(args){|a, b| "(#{literal(a)} * POWER(2, #{literal(b)}))"}
225
309
  when :>>
226
- "(#{literal(args[0])} / POWER(2, #{literal(args[1])}))"
310
+ sql << complex_expression_arg_pairs(args){|a, b| "(#{literal(a)} / POWER(2, #{literal(b)}))"}
311
+ when :extract
312
+ part = args.at(0)
313
+ raise(Sequel::Error, "unsupported extract argument: #{part.inspect}") unless format = EXTRACT_MAP[part]
314
+ if part == :second
315
+ expr = literal(args.at(1))
316
+ sql << DATEPART_SECOND_OPEN << format.to_s << COMMA << expr << DATEPART_SECOND_MIDDLE << expr << DATEPART_SECOND_CLOSE
317
+ else
318
+ sql << DATEPART_OPEN << format.to_s << COMMA
319
+ literal_append(sql, args.at(1))
320
+ sql << PAREN_CLOSE
321
+ end
227
322
  else
228
- super(op, args)
323
+ super
229
324
  end
230
325
  end
231
326
 
232
327
  # MSSQL doesn't support the SQL standard CURRENT_DATE or CURRENT_TIME
233
- def constant_sql(constant)
234
- CONSTANT_MAP[constant] || super
328
+ def constant_sql_append(sql, constant)
329
+ if c = CONSTANT_MAP[constant]
330
+ sql << c
331
+ else
332
+ super
333
+ end
235
334
  end
236
335
 
237
336
  # Disable the use of INSERT OUTPUT
@@ -244,21 +343,16 @@ module Sequel
244
343
  mutation_method(:disable_insert_output)
245
344
  end
246
345
 
247
- # When returning all rows, if an offset is used, delete the row_number column
248
- # before yielding the row.
249
- def fetch_rows(sql, &block)
250
- @opts[:offset] ? super(sql){|r| r.delete(row_number_column); yield r} : super(sql, &block)
251
- end
252
-
253
346
  # MSSQL uses the CONTAINS keyword for full text search
254
347
  def full_text_search(cols, terms, opts = {})
255
- filter("CONTAINS (#{literal(cols)}, #{literal(terms)})")
348
+ terms = "\"#{terms.join('" OR "')}\"" if terms.is_a?(Array)
349
+ filter("CONTAINS (?, ?)", cols, terms)
256
350
  end
257
351
 
258
352
  # Use the OUTPUT clause to get the value of all columns for the newly inserted record.
259
353
  def insert_select(*values)
260
- return unless supports_output_clause?
261
- naked.clone(default_server_opts(:sql=>output(nil, [:inserted.*]).insert_sql(*values))).single_record unless opts[:disable_insert_output]
354
+ return unless supports_insert_select?
355
+ naked.clone(default_server_opts(:sql=>output(nil, [SQL::ColumnAll.new(:inserted)]).insert_sql(*values))).single_record
262
356
  end
263
357
 
264
358
  # Specify a table for a SELECT ... INTO query.
@@ -266,17 +360,18 @@ module Sequel
266
360
  clone(:into => table)
267
361
  end
268
362
 
269
- # SQL Server does not support CTEs on subqueries, so move any CTEs
270
- # on joined datasets to the top level. The user is responsible for
271
- # resolving any name clashes this may cause.
272
- def join_table(type, table, expr=nil, table_alias={}, &block)
273
- return super unless Dataset === table && table.opts[:with]
274
- clone(:with => (opts[:with] || []) + table.opts[:with]).join_table(type, table.clone(:with => nil), expr, table_alias, &block)
275
- end
276
-
277
363
  # MSSQL uses a UNION ALL statement to insert multiple values at once.
278
364
  def multi_insert_sql(columns, values)
279
- [insert_sql(columns, LiteralString.new(values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")))]
365
+ c = false
366
+ sql = LiteralString.new('')
367
+ u = UNION_ALL
368
+ values.each do |v|
369
+ sql << u if c
370
+ sql << SELECT_SPACE
371
+ expression_list_append(sql, v)
372
+ c ||= true
373
+ end
374
+ [insert_sql(columns, sql)]
280
375
  end
281
376
 
282
377
  # Allows you to do a dirty read of uncommitted data using WITH (NOLOCK).
@@ -315,38 +410,30 @@ module Sequel
315
410
  end
316
411
 
317
412
  # MSSQL uses [] to quote identifiers
318
- def quoted_identifier(name)
319
- "[#{name}]"
413
+ def quoted_identifier_append(sql, name)
414
+ sql << BRACKET_OPEN << name.to_s << BRACKET_CLOSE
320
415
  end
321
416
 
322
- # MSSQL Requires the use of the ROW_NUMBER window function to emulate
323
- # an offset. This implementation requires MSSQL 2005 or greater (offset
324
- # can't be emulated well in MSSQL 2000).
325
- #
326
- # The implementation is ugly, cloning the current dataset and modifying
327
- # the clone to add a ROW_NUMBER window function (and some other things),
328
- # then using the modified clone in a subselect which is selected from.
329
- #
330
- # If offset is used, an order must be provided, because the use of ROW_NUMBER
331
- # requires an order.
332
- def select_sql
333
- return super unless o = @opts[:offset]
334
- raise(Error, 'MSSQL requires an order be provided if using an offset') unless order = @opts[:order]
335
- dsa1 = dataset_alias(1)
336
- rn = row_number_column
337
- subselect_sql(unlimited.
338
- unordered.
339
- select_append{ROW_NUMBER(:over, :order=>order){}.as(rn)}.
340
- from_self(:alias=>dsa1).
341
- limit(@opts[:limit]).
342
- where(SQL::Identifier.new(rn) > o))
343
- end
344
-
345
417
  # The version of the database server.
346
418
  def server_version
347
419
  db.server_version(@opts[:server])
348
420
  end
349
421
 
422
+ # MSSQL 2005+ supports GROUP BY CUBE.
423
+ def supports_group_cube?
424
+ is_2005_or_later?
425
+ end
426
+
427
+ # MSSQL 2005+ supports GROUP BY ROLLUP
428
+ def supports_group_rollup?
429
+ is_2005_or_later?
430
+ end
431
+
432
+ # MSSQL supports insert_select via the OUTPUT clause.
433
+ def supports_insert_select?
434
+ supports_output_clause? && !opts[:disable_insert_output]
435
+ end
436
+
350
437
  # MSSQL 2005+ supports INTERSECT and EXCEPT
351
438
  def supports_intersect_except?
352
439
  is_2005_or_later?
@@ -381,14 +468,38 @@ module Sequel
381
468
  def supports_window_functions?
382
469
  true
383
470
  end
471
+
472
+ # MSSQL cannot use WHERE 1.
473
+ def supports_where_true?
474
+ false
475
+ end
384
476
 
385
477
  protected
478
+
479
+ # If returned primary keys are requested, use OUTPUT unless already set on the
480
+ # dataset. If OUTPUT is already set, use existing returning values. If OUTPUT
481
+ # is only set to return a single columns, return an array of just that column.
482
+ # Otherwise, return an array of hashes.
483
+ def _import(columns, values, opts={})
484
+ if opts[:return] == :primary_key && !@opts[:output]
485
+ output(nil, [SQL::QualifiedIdentifier.new(:inserted, first_primary_key)])._import(columns, values, opts)
486
+ elsif @opts[:output]
487
+ statements = multi_insert_sql(columns, values)
488
+ @db.transaction(opts.merge(:server=>@opts[:server])) do
489
+ statements.map{|st| with_sql(st)}
490
+ end.first.map{|v| v.length == 1 ? v.values.first : v}
491
+ else
492
+ super
493
+ end
494
+ end
495
+
386
496
  # MSSQL does not allow ordering in sub-clauses unless 'top' (limit) is specified
387
497
  def aggregate_dataset
388
498
  (options_overlap(Sequel::Dataset::COUNT_FROM_SELF_OPTS) && !options_overlap([:limit])) ? unordered.from_self : super
389
499
  end
390
500
 
391
501
  private
502
+
392
503
  def is_2005_or_later?
393
504
  server_version >= 9000000
394
505
  end
@@ -401,7 +512,8 @@ module Sequel
401
512
 
402
513
  # Only include the primary table in the main delete clause
403
514
  def delete_from_sql(sql)
404
- sql << " FROM #{source_list(@opts[:from][0..0])}"
515
+ sql << FROM
516
+ source_list_append(sql, @opts[:from][0..0])
405
517
  end
406
518
 
407
519
  # MSSQL supports FROM clauses in DELETE and UPDATE statements.
@@ -413,19 +525,17 @@ module Sequel
413
525
  end
414
526
  alias update_from_sql delete_from2_sql
415
527
 
416
- # Handle the with clause for delete, insert, and update statements
417
- # to be the same as the insert statement.
418
- def delete_with_sql(sql)
419
- select_with_sql(sql)
528
+ # Return the first primary key for the current table. If this table has
529
+ # multiple primary keys, this will only return one of them. Used by #_import.
530
+ def first_primary_key
531
+ @db.schema(self).map{|k, v| k if v[:primary_key] == true}.compact.first
420
532
  end
421
- alias insert_with_sql delete_with_sql
422
- alias update_with_sql delete_with_sql
423
-
533
+
424
534
  # MSSQL raises an error if you try to provide more than 3 decimal places
425
535
  # for a fractional timestamp. This probably doesn't work for smalldatetime
426
536
  # fields.
427
537
  def format_timestamp_usec(usec)
428
- sprintf(".%03d", usec/1000)
538
+ sprintf(TIMESTAMP_USEC_FORMAT, usec/1000)
429
539
  end
430
540
 
431
541
  # MSSQL supports the OUTPUT clause for INSERT statements.
@@ -434,16 +544,26 @@ module Sequel
434
544
  INSERT_CLAUSE_METHODS
435
545
  end
436
546
 
547
+ # Use OUTPUT INSERTED.* to return all columns of the inserted row,
548
+ # for use with the prepared statement code.
549
+ def insert_output_sql(sql)
550
+ if @opts.has_key?(:returning)
551
+ sql << OUTPUT_INSERTED
552
+ else
553
+ output_sql(sql)
554
+ end
555
+ end
556
+
437
557
  # MSSQL uses a literal hexidecimal number for blob strings
438
- def literal_blob(v)
439
- blob = '0x'
440
- v.each_byte{|x| blob << sprintf('%02x', x)}
441
- blob
558
+ def literal_blob_append(sql, v)
559
+ sql << HEX_START << v.unpack(HSTAR).first
442
560
  end
443
561
 
444
- # Use unicode string syntax for all strings. Don't double backslashes.
445
- def literal_string(v)
446
- "N'#{v.gsub(/'/, "''")}'"
562
+ # Optionally use unicode string syntax for all strings. Don't double
563
+ # backslashes.
564
+ def literal_string_append(sql, v)
565
+ sql << (mssql_unicode_strings ? UNICODE_STRING_START : APOS)
566
+ sql << v.gsub(APOS_RE, DOUBLE_APOS) << APOS
447
567
  end
448
568
 
449
569
  # Use 0 for false on MSSQL
@@ -456,27 +576,30 @@ module Sequel
456
576
  BOOL_TRUE
457
577
  end
458
578
 
459
- # The alias to use for the row_number column when emulating OFFSET
460
- def row_number_column
461
- :x_sequel_row_number_x
462
- end
463
-
464
579
  # MSSQL adds the limit before the columns
465
580
  def select_clause_methods
466
581
  SELECT_CLAUSE_METHODS
467
582
  end
468
583
 
469
584
  def select_into_sql(sql)
470
- sql << " INTO #{table_ref(@opts[:into])}" if @opts[:into]
585
+ if i = @opts[:into]
586
+ sql << INTO
587
+ table_ref_append(sql, i)
588
+ end
471
589
  end
472
590
 
473
591
  # MSSQL uses TOP N for limit. For MSSQL 2005+ TOP (N) is used
474
592
  # to allow the limit to be a bound variable.
475
593
  def select_limit_sql(sql)
476
594
  if l = @opts[:limit]
477
- l = literal(l)
478
- l = "(#{l})" if server_version >= 9000000
479
- sql << " TOP #{l}"
595
+ if is_2005_or_later?
596
+ sql << TOP_PAREN
597
+ literal_append(sql, l)
598
+ sql << PAREN_CLOSE
599
+ else
600
+ sql << TOP
601
+ literal_append(sql, l)
602
+ end
480
603
  end
481
604
  end
482
605
 
@@ -496,19 +619,20 @@ module Sequel
496
619
  def output_sql(sql)
497
620
  return unless supports_output_clause?
498
621
  return unless output = @opts[:output]
499
- sql << " OUTPUT #{column_list(output[:select_list])}"
622
+ sql << OUTPUT
623
+ column_list_append(sql, output[:select_list])
500
624
  if into = output[:into]
501
- sql << " INTO #{table_ref(into)}"
625
+ sql << INTO
626
+ table_ref_append(sql, into)
502
627
  if column_list = output[:column_list]
503
- cl = []
504
- column_list.each { |k, v| cl << literal(String === k ? k.to_sym : k) }
505
- sql << " (#{cl.join(COMMA_SEPARATOR)})"
628
+ sql << PAREN_SPACE_OPEN
629
+ source_list_append(sql, column_list)
630
+ sql << PAREN_CLOSE
506
631
  end
507
632
  end
508
633
  end
509
634
  alias delete_output_sql output_sql
510
635
  alias update_output_sql output_sql
511
- alias insert_output_sql output_sql
512
636
 
513
637
  # MSSQL supports the OUTPUT clause for UPDATE statements.
514
638
  # It also allows prepending a WITH clause.
@@ -518,7 +642,8 @@ module Sequel
518
642
 
519
643
  # Only include the primary table in the main update clause
520
644
  def update_table_sql(sql)
521
- sql << " #{source_list(@opts[:from][0..0])}"
645
+ sql << SPACE
646
+ source_list_append(sql, @opts[:from][0..0])
522
647
  end
523
648
  end
524
649
  end