sequel 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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