sequel 3.4.0 → 3.5.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 (93) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +1 -1
  3. data/doc/cheat_sheet.rdoc +5 -2
  4. data/doc/opening_databases.rdoc +2 -0
  5. data/doc/release_notes/3.5.0.txt +510 -0
  6. data/lib/sequel/adapters/ado.rb +3 -1
  7. data/lib/sequel/adapters/ado/mssql.rb +2 -2
  8. data/lib/sequel/adapters/do.rb +2 -11
  9. data/lib/sequel/adapters/do/mysql.rb +7 -0
  10. data/lib/sequel/adapters/do/postgres.rb +2 -2
  11. data/lib/sequel/adapters/firebird.rb +3 -3
  12. data/lib/sequel/adapters/informix.rb +3 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +3 -3
  14. data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
  15. data/lib/sequel/adapters/mysql.rb +60 -21
  16. data/lib/sequel/adapters/odbc.rb +1 -1
  17. data/lib/sequel/adapters/openbase.rb +3 -3
  18. data/lib/sequel/adapters/oracle.rb +1 -5
  19. data/lib/sequel/adapters/postgres.rb +3 -3
  20. data/lib/sequel/adapters/shared/mssql.rb +142 -33
  21. data/lib/sequel/adapters/shared/mysql.rb +54 -31
  22. data/lib/sequel/adapters/shared/oracle.rb +17 -6
  23. data/lib/sequel/adapters/shared/postgres.rb +7 -7
  24. data/lib/sequel/adapters/shared/progress.rb +3 -3
  25. data/lib/sequel/adapters/shared/sqlite.rb +3 -17
  26. data/lib/sequel/connection_pool.rb +4 -6
  27. data/lib/sequel/core.rb +29 -113
  28. data/lib/sequel/database.rb +14 -12
  29. data/lib/sequel/dataset.rb +8 -21
  30. data/lib/sequel/dataset/convenience.rb +1 -1
  31. data/lib/sequel/dataset/graph.rb +9 -2
  32. data/lib/sequel/dataset/sql.rb +170 -104
  33. data/lib/sequel/exceptions.rb +3 -0
  34. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  35. data/lib/sequel/extensions/named_timezones.rb +61 -0
  36. data/lib/sequel/extensions/schema_dumper.rb +7 -1
  37. data/lib/sequel/extensions/sql_expr.rb +122 -0
  38. data/lib/sequel/extensions/string_date_time.rb +4 -4
  39. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  40. data/lib/sequel/model/associations.rb +105 -45
  41. data/lib/sequel/model/base.rb +37 -28
  42. data/lib/sequel/plugins/active_model.rb +35 -0
  43. data/lib/sequel/plugins/association_dependencies.rb +96 -0
  44. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  45. data/lib/sequel/plugins/force_encoding.rb +61 -0
  46. data/lib/sequel/plugins/many_through_many.rb +32 -11
  47. data/lib/sequel/plugins/nested_attributes.rb +7 -2
  48. data/lib/sequel/plugins/subclasses.rb +45 -0
  49. data/lib/sequel/plugins/touch.rb +118 -0
  50. data/lib/sequel/plugins/typecast_on_load.rb +61 -0
  51. data/lib/sequel/sql.rb +31 -30
  52. data/lib/sequel/timezones.rb +161 -0
  53. data/lib/sequel/version.rb +1 -1
  54. data/spec/adapters/mssql_spec.rb +262 -0
  55. data/spec/adapters/mysql_spec.rb +46 -8
  56. data/spec/adapters/postgres_spec.rb +6 -3
  57. data/spec/adapters/spec_helper.rb +21 -0
  58. data/spec/adapters/sqlite_spec.rb +1 -1
  59. data/spec/core/connection_pool_spec.rb +1 -1
  60. data/spec/core/database_spec.rb +27 -1
  61. data/spec/core/dataset_spec.rb +63 -1
  62. data/spec/core/object_graph_spec.rb +1 -1
  63. data/spec/core/schema_spec.rb +1 -0
  64. data/spec/extensions/active_model_spec.rb +47 -0
  65. data/spec/extensions/association_dependencies_spec.rb +108 -0
  66. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  67. data/spec/extensions/force_encoding_spec.rb +75 -0
  68. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  69. data/spec/extensions/many_through_many_spec.rb +60 -2
  70. data/spec/extensions/named_timezones_spec.rb +72 -0
  71. data/spec/extensions/nested_attributes_spec.rb +29 -1
  72. data/spec/extensions/schema_dumper_spec.rb +10 -0
  73. data/spec/extensions/spec_helper.rb +1 -1
  74. data/spec/extensions/sql_expr_spec.rb +89 -0
  75. data/spec/extensions/subclasses_spec.rb +52 -0
  76. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  77. data/spec/extensions/touch_spec.rb +155 -0
  78. data/spec/extensions/typecast_on_load_spec.rb +60 -0
  79. data/spec/integration/database_test.rb +8 -0
  80. data/spec/integration/dataset_test.rb +9 -9
  81. data/spec/integration/plugin_test.rb +139 -0
  82. data/spec/integration/schema_test.rb +7 -7
  83. data/spec/integration/spec_helper.rb +32 -1
  84. data/spec/integration/timezone_test.rb +3 -3
  85. data/spec/integration/transaction_test.rb +1 -1
  86. data/spec/integration/type_test.rb +6 -6
  87. data/spec/model/association_reflection_spec.rb +18 -0
  88. data/spec/model/associations_spec.rb +169 -9
  89. data/spec/model/base_spec.rb +2 -0
  90. data/spec/model/eager_loading_spec.rb +82 -2
  91. data/spec/model/model_spec.rb +8 -1
  92. data/spec/model/record_spec.rb +52 -9
  93. metadata +33 -23
@@ -23,10 +23,12 @@ module Sequel
23
23
  # to execute a command before cancelling the attempt and generating
24
24
  # an error. Specifically, it sets the ADO CommandTimeout property.
25
25
  # If this property is not set, the default of 30 seconds is used.
26
+ # * :conn_string - The full ADO connection string. If this is provided,
27
+ # the usual options are ignored.
26
28
  # * :provider - Sets the Provider of this ADO connection (for example, "SQLOLEDB")
27
29
  def connect(server)
28
30
  opts = server_opts(server)
29
- s = "driver=#{opts[:driver]};server=#{opts[:host]};database=#{opts[:database]}#{";uid=#{opts[:user]};pwd=#{opts[:password]}" if opts[:user]}"
31
+ s = opts[:conn_string] || "driver=#{opts[:driver]};server=#{opts[:host]};database=#{opts[:database]}#{";uid=#{opts[:user]};pwd=#{opts[:password]}" if opts[:user]}"
30
32
  handle = WIN32OLE.new('ADODB.Connection')
31
33
  handle.CommandTimeout = opts[:command_timeout] if opts[:command_timeout]
32
34
  handle.Provider = opts[:provider] if opts[:provider]
@@ -20,9 +20,9 @@ module Sequel
20
20
  # Use a nasty hack of multiple SQL statements in the same call and
21
21
  # having the last one return the most recently inserted id. This
22
22
  # is necessary as ADO doesn't provide a consistent native connection.
23
- def insert(values={})
23
+ def insert(*values)
24
24
  return super if @opts[:sql]
25
- with_sql("SET NOCOUNT ON; #{insert_sql(values)}; SELECT CAST(SCOPE_IDENTITY() AS INTEGER)").single_value
25
+ with_sql("SET NOCOUNT ON; #{insert_sql(*values)}; SELECT CAST(SCOPE_IDENTITY() AS INTEGER)").single_value
26
26
  end
27
27
  end
28
28
  end
@@ -16,19 +16,16 @@ module Sequel
16
16
  DATABASE_SETUP = {:postgres=>proc do |db|
17
17
  require 'do_postgres'
18
18
  Sequel.require 'adapters/do/postgres'
19
- db.converted_exceptions << PostgresError
20
19
  db.extend(Sequel::DataObjects::Postgres::DatabaseMethods)
21
20
  end,
22
21
  :mysql=>proc do |db|
23
22
  require 'do_mysql'
24
23
  Sequel.require 'adapters/do/mysql'
25
- db.converted_exceptions << MysqlError
26
24
  db.extend(Sequel::DataObjects::MySQL::DatabaseMethods)
27
25
  end,
28
26
  :sqlite3=>proc do |db|
29
27
  require 'do_sqlite3'
30
28
  Sequel.require 'adapters/do/sqlite'
31
- db.converted_exceptions << Sqlite3Error
32
29
  db.extend(Sequel::DataObjects::SQLite::DatabaseMethods)
33
30
  end
34
31
  }
@@ -41,18 +38,12 @@ module Sequel
41
38
  class Database < Sequel::Database
42
39
  set_adapter_scheme :do
43
40
 
44
- # Convert the given exceptions to Sequel:Errors, necessary
45
- # because DO raises errors specific to database types in
46
- # certain cases.
47
- attr_accessor :converted_exceptions
48
-
49
41
  # Call the DATABASE_SETUP proc directly after initialization,
50
42
  # so the object always uses sub adapter specific code. Also,
51
43
  # raise an error immediately if the connection doesn't have a
52
44
  # uri, since DataObjects requires one.
53
45
  def initialize(opts)
54
46
  @opts = opts
55
- @converted_exceptions = []
56
47
  raise(Error, "No connection string specified") unless uri
57
48
  if prok = DATABASE_SETUP[subadapter.to_sym]
58
49
  prok.call(self)
@@ -81,8 +72,8 @@ module Sequel
81
72
  begin
82
73
  command = conn.create_command(sql)
83
74
  res = block_given? ? command.execute_reader : command.execute_non_query
84
- rescue Exception => e
85
- raise_error(e, :classes=>@converted_exceptions)
75
+ rescue ::DataObjects::Error => e
76
+ raise_error(e)
86
77
  end
87
78
  if block_given?
88
79
  begin
@@ -32,6 +32,13 @@ module Sequel
32
32
  def replace(*args)
33
33
  execute_insert(replace_sql(*args))
34
34
  end
35
+
36
+ private
37
+
38
+ # do_mysql sets NO_BACKSLASH_ESCAPES, so use standard SQL string escaping
39
+ def literal_string(s)
40
+ "'#{s.gsub("'", "''")}'"
41
+ end
35
42
  end
36
43
  end
37
44
  end
@@ -1,7 +1,7 @@
1
1
  Sequel.require 'adapters/shared/postgres'
2
2
 
3
3
  module Sequel
4
- Postgres::CONVERTED_EXCEPTIONS << PostgresError
4
+ Postgres::CONVERTED_EXCEPTIONS << ::DataObjects::Error
5
5
 
6
6
  module DataObjects
7
7
  # Adapter, Database, and Dataset support for accessing a PostgreSQL
@@ -27,7 +27,7 @@ module Sequel
27
27
  else
28
28
  command.execute_non_query
29
29
  end
30
- rescue PostgresError => e
30
+ rescue ::DataObjects::Error => e
31
31
  raise_error(e)
32
32
  end
33
33
  end
@@ -202,7 +202,7 @@ module Sequel
202
202
  BOOL_FALSE = '0'.freeze
203
203
  NULL = LiteralString.new('NULL').freeze
204
204
  COMMA_SEPARATOR = ', '.freeze
205
- SELECT_CLAUSE_ORDER = %w'with distinct limit columns from join where group having compounds order'.freeze
205
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct limit columns from join where group having compounds order')
206
206
 
207
207
  # Yield all rows returned by executing the given SQL and converting
208
208
  # the types.
@@ -253,8 +253,8 @@ module Sequel
253
253
  end
254
254
 
255
255
  # The order of clauses in the SELECT SQL statement
256
- def select_clause_order
257
- SELECT_CLAUSE_ORDER
256
+ def select_clause_methods
257
+ SELECT_CLAUSE_METHODS
258
258
  end
259
259
 
260
260
  def select_limit_sql(sql)
@@ -37,7 +37,7 @@ module Sequel
37
37
  end
38
38
 
39
39
  class Dataset < Sequel::Dataset
40
- SELECT_CLAUSE_ORDER = %w'limit distinct columns from join where having group compounds order'.freeze
40
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'limit distinct columns from join where having group compounds order')
41
41
 
42
42
  def fetch_rows(sql, &block)
43
43
  execute(sql) do |cursor|
@@ -66,8 +66,8 @@ module Sequel
66
66
  false
67
67
  end
68
68
 
69
- def select_clause_order
70
- SELECT_CLAUSE_ORDER
69
+ def select_clause_methods
70
+ SELECT_CLAUSE_METHODS
71
71
  end
72
72
 
73
73
  def select_limit_sql(sql)
@@ -74,7 +74,7 @@ module Sequel
74
74
 
75
75
  # Dataset class for H2 datasets accessed via JDBC.
76
76
  class Dataset < JDBC::Dataset
77
- SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
77
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'distinct columns from join where group having compounds order limit')
78
78
 
79
79
  # H2 requires SQL standard datetimes
80
80
  def requires_sql_standard_datetimes?
@@ -88,8 +88,8 @@ module Sequel
88
88
 
89
89
  private
90
90
 
91
- def select_clause_order
92
- SELECT_CLAUSE_ORDER
91
+ def select_clause_methods
92
+ SELECT_CLAUSE_METHODS
93
93
  end
94
94
  end
95
95
  end
@@ -12,6 +12,8 @@ module Sequel
12
12
  module MSSQL
13
13
  # Database instance methods for MSSQL databases accessed via JDBC.
14
14
  module DatabaseMethods
15
+ PRIMARY_KEY_INDEX_RE = /\Apk__/i.freeze
16
+
15
17
  include Sequel::MSSQL::DatabaseMethods
16
18
 
17
19
  # Return instance of Sequel::JDBC::MSSQL::Dataset with the given opts.
@@ -40,6 +42,11 @@ module Sequel
40
42
  def schema_parse_table(table, opts={})
41
43
  jdbc_schema_parse_table(table, opts)
42
44
  end
45
+
46
+ # Primary key indexes appear to start with pk__ on MSSQL
47
+ def primary_key_index_re
48
+ PRIMARY_KEY_INDEX_RE
49
+ end
43
50
  end
44
51
 
45
52
  # Dataset class for MSSQL datasets accessed via JDBC.
@@ -67,7 +67,7 @@ module Sequel
67
67
  include Sequel::MySQL::DatabaseMethods
68
68
 
69
69
  # Mysql::Error messages that indicate the current connection should be disconnected
70
- MYSQL_DATABASE_DISCONNECT_ERRORS = /\ACommands out of sync; you can't run this command now\z/
70
+ MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now\z|Can't connect to local MySQL server through socket)/
71
71
 
72
72
  set_adapter_scheme :mysql
73
73
 
@@ -132,12 +132,12 @@ module Sequel
132
132
  # Executes the given SQL using an available connection, yielding the
133
133
  # connection if the block is given.
134
134
  def execute(sql, opts={}, &block)
135
- return call_sproc(sql, opts, &block) if opts[:sproc]
136
- return execute_prepared_statement(sql, opts, &block) if Symbol === sql
137
- begin
135
+ if opts[:sproc]
136
+ call_sproc(sql, opts, &block)
137
+ elsif sql.is_a?(Symbol)
138
+ execute_prepared_statement(sql, opts, &block)
139
+ else
138
140
  synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
139
- rescue Mysql::Error => e
140
- raise_error(e)
141
141
  end
142
142
  end
143
143
 
@@ -178,7 +178,16 @@ module Sequel
178
178
  rescue Mysql::Error => e
179
179
  raise_error(e, :disconnect=>MYSQL_DATABASE_DISCONNECT_ERRORS.match(e.message))
180
180
  ensure
181
- r.free if r
181
+ if r
182
+ r.free
183
+ # Use up all results to avoid a commands out of sync message.
184
+ if conn.respond_to?(:next_result)
185
+ while conn.next_result
186
+ r = conn.use_result
187
+ r.free if r
188
+ end
189
+ end
190
+ end
182
191
  end
183
192
  end
184
193
 
@@ -221,16 +230,10 @@ module Sequel
221
230
  synchronize(opts[:server]) do |conn|
222
231
  unless conn.prepared_statements[ps_name] == sql
223
232
  conn.prepared_statements[ps_name] = sql
224
- s = "PREPARE #{ps_name} FROM '#{::Mysql.quote(sql)}'"
225
- log_info(s)
226
- conn.query(s)
233
+ _execute(conn, "PREPARE #{ps_name} FROM '#{::Mysql.quote(sql)}'", opts)
227
234
  end
228
235
  i = 0
229
- args.each do |arg|
230
- s = "SET @sequel_arg_#{i+=1} = #{literal(arg)}"
231
- log_info(s)
232
- conn.query(s)
233
- end
236
+ _execute(conn, "SET " + args.map {|arg| "@sequel_arg_#{i+=1} = #{literal(arg)}"}.join(", "), opts) unless args.empty?
234
237
  _execute(conn, "EXECUTE #{ps_name}#{" USING #{(1..i).map{|j| "@sequel_arg_#{j}"}.join(', ')}" unless i == 0}", opts, &block)
235
238
  end
236
239
  end
@@ -310,21 +313,31 @@ module Sequel
310
313
  execute_dui(delete_sql){|c| c.affected_rows}
311
314
  end
312
315
 
313
- # Yield all rows matching this dataset
314
- def fetch_rows(sql)
316
+ # Yield all rows matching this dataset. If the dataset is set to
317
+ # split multiple statements, yield arrays of hashes one per statement
318
+ # instead of yielding results for all statements as hashes.
319
+ def fetch_rows(sql, &block)
315
320
  execute(sql) do |r|
316
321
  i = -1
317
322
  cols = r.fetch_fields.map{|f| [output_identifier(f.name), MYSQL_TYPES[f.type], i+=1]}
318
323
  @columns = cols.map{|c| c.first}
319
- while row = r.fetch_row
320
- h = {}
321
- cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v}
322
- yield h
324
+ if opts[:split_multiple_result_sets]
325
+ s = []
326
+ yield_rows(r, cols){|h| s << h}
327
+ yield s
328
+ else
329
+ yield_rows(r, cols, &block)
323
330
  end
324
331
  end
325
332
  self
326
333
  end
327
334
 
335
+ # Don't allow graphing a dataset that splits multiple statements
336
+ def graph(*)
337
+ raise(Error, "Can't graph a dataset that splits multiple result sets") if opts[:split_multiple_result_sets]
338
+ super
339
+ end
340
+
328
341
  # Insert a new value into this dataset
329
342
  def insert(*values)
330
343
  execute_dui(insert_sql(*values)){|c| c.insert_id}
@@ -347,6 +360,22 @@ module Sequel
347
360
  execute_dui(replace_sql(*args)){|c| c.insert_id}
348
361
  end
349
362
 
363
+ # Makes each yield arrays of rows, with each array containing the rows
364
+ # for a given result set. Does not work with graphing. So you can submit
365
+ # SQL with multiple statements and easily determine which statement
366
+ # returned which results.
367
+ #
368
+ # Modifies the row_proc of the returned dataset so that it still works
369
+ # as expected (running on the hashes instead of on the arrays of hashes).
370
+ # If you modify the row_proc afterward, note that it will receive an array
371
+ # of hashes instead of a hash.
372
+ def split_multiple_result_sets
373
+ raise(Error, "Can't split multiple statements on a graphed dataset") if opts[:graph]
374
+ ds = clone(:split_multiple_result_sets=>true)
375
+ ds.row_proc = proc{|x| x.map{|h| row_proc.call(h)}} if row_proc
376
+ ds
377
+ end
378
+
350
379
  # Update the matching rows.
351
380
  def update(values={})
352
381
  execute_dui(update_sql(values)){|c| c.affected_rows}
@@ -373,6 +402,16 @@ module Sequel
373
402
  def prepare_extend_sproc(ds)
374
403
  ds.extend(StoredProcedureMethods)
375
404
  end
405
+
406
+ # Yield each row of the given result set r with columns cols
407
+ # as a hash with symbol keys
408
+ def yield_rows(r, cols)
409
+ while row = r.fetch_row
410
+ h = {}
411
+ cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v}
412
+ yield h
413
+ end
414
+ end
376
415
  end
377
416
  end
378
417
  end
@@ -90,7 +90,7 @@ module Sequel
90
90
  BOOL_TRUE = '1'.freeze
91
91
  BOOL_FALSE = '0'.freeze
92
92
  ODBC_DATE_FORMAT = "{d '%Y-%m-%d'}".freeze
93
- TIMESTAMP_FORMAT="{ts '%Y-%m-%d %H:%M:%S%N'}".freeze
93
+ TIMESTAMP_FORMAT="{ts '%Y-%m-%d %H:%M:%S'}".freeze
94
94
 
95
95
  def fetch_rows(sql, &block)
96
96
  execute(sql) do |s|
@@ -37,7 +37,7 @@ module Sequel
37
37
  end
38
38
 
39
39
  class Dataset < Sequel::Dataset
40
- SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
40
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'distinct columns from join where group having compounds order limit')
41
41
 
42
42
  def fetch_rows(sql)
43
43
  execute(sql) do |result|
@@ -57,8 +57,8 @@ module Sequel
57
57
 
58
58
  private
59
59
 
60
- def select_clause_order
61
- SELECT_CLAUSE_ORDER
60
+ def select_clause_methods
61
+ SELECT_CLAUSE_METHODS
62
62
  end
63
63
  end
64
64
  end
@@ -67,11 +67,7 @@ module Sequel
67
67
  yield(r) if block_given?
68
68
  r
69
69
  rescue OCIException => e
70
- if CONNECTION_ERROR_CODES.include?(e.code)
71
- raise(Sequel::DatabaseDisconnectError)
72
- else
73
- raise
74
- end
70
+ raise_error(e, :disconnect=>CONNECTION_ERROR_CODES.include?(e.code))
75
71
  end
76
72
  end
77
73
  end
@@ -144,14 +144,14 @@ module Sequel
144
144
  q = nil
145
145
  begin
146
146
  q = args ? async_exec(sql, args) : async_exec(sql)
147
- rescue PGError
147
+ rescue PGError => e
148
148
  begin
149
149
  s = status
150
150
  rescue PGError
151
- raise(Sequel::DatabaseDisconnectError)
151
+ raise Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)
152
152
  end
153
153
  status_ok = (s == Adapter::CONNECTION_OK)
154
- status_ok ? raise : raise(Sequel::DatabaseDisconnectError)
154
+ status_ok ? raise : Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)
155
155
  ensure
156
156
  block if status_ok
157
157
  end
@@ -2,6 +2,8 @@ module Sequel
2
2
  module MSSQL
3
3
  module DatabaseMethods
4
4
  AUTO_INCREMENT = 'IDENTITY(1,1)'.freeze
5
+ SERVER_VERSION_RE = /^(\d+)\.(\d+)\.(\d+)/.freeze
6
+ SERVER_VERSION_SQL = "SELECT CAST(SERVERPROPERTY('ProductVersion') AS varchar)".freeze
5
7
  SQL_BEGIN = "BEGIN TRANSACTION".freeze
6
8
  SQL_COMMIT = "COMMIT TRANSACTION".freeze
7
9
  SQL_ROLLBACK = "ROLLBACK TRANSACTION".freeze
@@ -13,7 +15,26 @@ module Sequel
13
15
  def database_type
14
16
  :mssql
15
17
  end
16
-
18
+
19
+ # The version of the MSSQL server, as an integer (e.g. 10001600 for
20
+ # SQL Server 2008 Express).
21
+ def server_version(server=nil)
22
+ return @server_version if @server_version
23
+ @server_version = synchronize(server) do |conn|
24
+ (conn.server_version rescue nil) if conn.respond_to?(:server_version)
25
+ end
26
+ unless @server_version
27
+ m = SERVER_VERSION_RE.match(fetch(SERVER_VERSION_SQL).single_value.to_s)
28
+ @server_version = (m[1].to_i * 1000000) + (m[2].to_i * 10000) + m[3].to_i
29
+ end
30
+ @server_version
31
+ end
32
+
33
+ # MSSQL supports savepoints, though it doesn't support committing/releasing them savepoint
34
+ def supports_savepoints?
35
+ true
36
+ end
37
+
17
38
  # Microsoft SQL Server supports using the INFORMATION_SCHEMA to get
18
39
  # information on tables.
19
40
  def tables(opts={})
@@ -23,11 +44,6 @@ module Sequel
23
44
  filter(:table_type=>'BASE TABLE', :table_schema=>(opts[:schema]||default_schema||'dbo').to_s).
24
45
  map{|x| m.call(x[:table_name])}
25
46
  end
26
-
27
- # MSSQL supports savepoints, though it doesn't support committing/releasing them savepoint
28
- def supports_savepoints?
29
- true
30
- end
31
47
 
32
48
  private
33
49
 
@@ -83,6 +99,13 @@ module Sequel
83
99
  "DROP INDEX #{quote_identifier(op[:name] || default_index_name(table, op[:columns]))} ON #{quote_schema_table(table)}"
84
100
  end
85
101
 
102
+ # Always quote identifiers in the metadata_dataset, so schema parsing works.
103
+ def metadata_dataset
104
+ ds = super
105
+ ds.quote_identifiers = true
106
+ ds
107
+ end
108
+
86
109
  # SQL to rollback to a savepoint
87
110
  def rollback_savepoint_sql(depth)
88
111
  SQL_ROLLBACK_TO_SAVEPOINT % depth
@@ -113,7 +136,7 @@ module Sequel
113
136
  [m.call(row.delete(:column)), row]
114
137
  end
115
138
  end
116
-
139
+
117
140
  # SQL fragment for marking a table as temporary
118
141
  def temporary_table_sql
119
142
  TEMPORARY
@@ -145,11 +168,14 @@ module Sequel
145
168
  module DatasetMethods
146
169
  BOOL_TRUE = '1'.freeze
147
170
  BOOL_FALSE = '0'.freeze
148
- SELECT_CLAUSE_ORDER = %w'with limit distinct columns from table_options join where group order having compounds'.freeze
149
- TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S'".freeze
171
+ COMMA_SEPARATOR = ', '.freeze
172
+ DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'with from output from2 where')
173
+ INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with into columns output values')
174
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with limit distinct columns from table_options join where group order having compounds')
175
+ UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'with table set output from where')
150
176
  WILDCARD = LiteralString.new('*').freeze
151
177
  CONSTANT_MAP = {:CURRENT_DATE=>'CAST(CURRENT_TIMESTAMP AS DATE)'.freeze, :CURRENT_TIME=>'CAST(CURRENT_TIMESTAMP AS TIME)'.freeze}
152
-
178
+
153
179
  # MSSQL uses + for string concatenation
154
180
  def complex_expression_sql(op, args)
155
181
  case op
@@ -167,8 +193,8 @@ module Sequel
167
193
 
168
194
  # When returning all rows, if an offset is used, delete the row_number column
169
195
  # before yielding the row.
170
- def each(&block)
171
- @opts[:offset] ? super{|r| r.delete(row_number_column); yield r} : super(&block)
196
+ def fetch_rows(sql, &block)
197
+ @opts[:offset] ? super(sql){|r| r.delete(row_number_column); yield r} : super(sql, &block)
172
198
  end
173
199
 
174
200
  # MSSQL uses the CONTAINS keyword for full text search
@@ -178,15 +204,43 @@ module Sequel
178
204
 
179
205
  # MSSQL uses a UNION ALL statement to insert multiple values at once.
180
206
  def multi_insert_sql(columns, values)
181
- values = values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")
182
- ["#{insert_sql_base}#{source_list(@opts[:from])} (#{identifier_list(columns)}) #{values}"]
207
+ [insert_sql(columns, LiteralString.new(values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")))]
183
208
  end
184
209
 
185
210
  # Allows you to do .nolock on a query
186
211
  def nolock
187
212
  clone(:table_options => "(NOLOCK)")
188
213
  end
189
-
214
+
215
+ # Include an OUTPUT clause in the eventual INSERT, UPDATE, or DELETE query.
216
+ #
217
+ # The first argument is the table to output into, and the second argument
218
+ # is either an Array of column values to select, or a Hash which maps output
219
+ # column names to selected values, in the style of #insert or #update.
220
+ #
221
+ # Output into a returned result set is not currently supported.
222
+ #
223
+ # Examples:
224
+ #
225
+ # dataset.output(:output_table, [:deleted__id, :deleted__name])
226
+ # dataset.output(:output_table, :id => :inserted__id, :name => :inserted__name)
227
+ def output(into, values)
228
+ output = {}
229
+ case values
230
+ when Hash:
231
+ output[:column_list], output[:select_list] = values.keys, values.values
232
+ when Array:
233
+ output[:select_list] = values
234
+ end
235
+ output[:into] = into
236
+ clone({:output => output})
237
+ end
238
+
239
+ # An output method that modifies the receiver.
240
+ def output!(into, values)
241
+ mutation_method(:output, into, values)
242
+ end
243
+
190
244
  # MSSQL uses [] to quote identifiers
191
245
  def quoted_identifier(name)
192
246
  "[#{name}]"
@@ -218,6 +272,11 @@ module Sequel
218
272
  select_sql
219
273
  end
220
274
 
275
+ # The version of the database server.
276
+ def server_version
277
+ db.server_version(@opts[:server])
278
+ end
279
+
221
280
  # Microsoft SQL Server does not support INTERSECT or EXCEPT
222
281
  def supports_intersect_except?
223
282
  false
@@ -227,19 +286,56 @@ module Sequel
227
286
  def supports_is_true?
228
287
  false
229
288
  end
230
-
231
- # MSSQL supports timezones in literal timestamps
232
- def supports_timestamp_timezones?
233
- true
234
- end
235
-
289
+
236
290
  # MSSQL 2005+ supports window functions
237
291
  def supports_window_functions?
238
292
  true
239
293
  end
240
294
 
241
295
  private
296
+
297
+ # MSSQL can modify joined datasets
298
+ def check_modification_allowed!
299
+ raise(InvalidOperation, "Grouped datasets cannot be modified") if opts[:group]
300
+ end
301
+
302
+ # MSSQL supports the OUTPUT clause for DELETE statements.
303
+ # It also allows prepending a WITH clause.
304
+ def delete_clause_methods
305
+ DELETE_CLAUSE_METHODS
306
+ end
307
+
308
+ # Handle the with clause for delete, insert, and update statements
309
+ # to be the same as the insert statement.
310
+ def delete_with_sql(sql)
311
+ select_with_sql(sql)
312
+ end
313
+ alias insert_with_sql delete_with_sql
314
+ alias update_with_sql delete_with_sql
242
315
 
316
+ # MSSQL raises an error if you try to provide more than 3 decimal places
317
+ # for a fractional timestamp. This probably doesn't work for smalldatetime
318
+ # fields.
319
+ def format_timestamp_usec(usec)
320
+ sprintf(".%03d", usec/1000)
321
+ end
322
+
323
+ # MSSQL supports FROM clauses in DELETE and UPDATE statements.
324
+ def from_sql(sql)
325
+ if (opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join]
326
+ select_from_sql(sql)
327
+ select_join_sql(sql)
328
+ end
329
+ end
330
+ alias delete_from2_sql from_sql
331
+ alias update_from_sql from_sql
332
+
333
+ # MSSQL supports the OUTPUT clause for INSERT statements.
334
+ # It also allows prepending a WITH clause.
335
+ def insert_clause_methods
336
+ INSERT_CLAUSE_METHODS
337
+ end
338
+
243
339
  # MSSQL uses a literal hexidecimal number for blob strings
244
340
  def literal_blob(v)
245
341
  blob = '0x'
@@ -252,20 +348,10 @@ module Sequel
252
348
  "N#{super}"
253
349
  end
254
350
 
255
- # Use MSSQL Timestamp format
256
- def literal_datetime(v)
257
- v.strftime(TIMESTAMP_FORMAT)
258
- end
259
-
260
351
  # Use 0 for false on MSSQL
261
352
  def literal_false
262
353
  BOOL_FALSE
263
354
  end
264
-
265
- # Use MSSQL Timestamp format
266
- def literal_time(v)
267
- v.strftime(TIMESTAMP_FORMAT)
268
- end
269
355
 
270
356
  # Use 1 for true on MSSQL
271
357
  def literal_true
@@ -278,8 +364,8 @@ module Sequel
278
364
  end
279
365
 
280
366
  # MSSQL adds the limit before the columns
281
- def select_clause_order
282
- SELECT_CLAUSE_ORDER
367
+ def select_clause_methods
368
+ SELECT_CLAUSE_METHODS
283
369
  end
284
370
 
285
371
  # MSSQL uses TOP for limit
@@ -291,6 +377,29 @@ module Sequel
291
377
  def select_table_options_sql(sql)
292
378
  sql << " WITH #{@opts[:table_options]}" if @opts[:table_options]
293
379
  end
380
+
381
+ # SQL fragment for MSSQL's OUTPUT clause.
382
+ def output_sql(sql)
383
+ return unless output = @opts[:output]
384
+ sql << " OUTPUT #{column_list(output[:select_list])}"
385
+ if into = output[:into]
386
+ sql << " INTO #{table_ref(into)}"
387
+ if column_list = output[:column_list]
388
+ cl = []
389
+ column_list.each { |k, v| cl << literal(String === k ? k.to_sym : k) }
390
+ sql << " (#{cl.join(COMMA_SEPARATOR)})"
391
+ end
392
+ end
393
+ end
394
+ alias delete_output_sql output_sql
395
+ alias update_output_sql output_sql
396
+ alias insert_output_sql output_sql
397
+
398
+ # MSSQL supports the OUTPUT clause for UPDATE statements.
399
+ # It also allows prepending a WITH clause.
400
+ def update_clause_methods
401
+ UPDATE_CLAUSE_METHODS
402
+ end
294
403
  end
295
404
  end
296
405
  end