sequel 3.2.0 → 3.3.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 (50) hide show
  1. data/CHANGELOG +40 -0
  2. data/Rakefile +1 -1
  3. data/doc/opening_databases.rdoc +7 -0
  4. data/doc/release_notes/3.3.0.txt +192 -0
  5. data/lib/sequel/adapters/ado.rb +34 -39
  6. data/lib/sequel/adapters/ado/mssql.rb +30 -0
  7. data/lib/sequel/adapters/jdbc.rb +27 -4
  8. data/lib/sequel/adapters/jdbc/h2.rb +14 -3
  9. data/lib/sequel/adapters/jdbc/mssql.rb +51 -0
  10. data/lib/sequel/adapters/mysql.rb +28 -12
  11. data/lib/sequel/adapters/odbc.rb +36 -30
  12. data/lib/sequel/adapters/odbc/mssql.rb +44 -0
  13. data/lib/sequel/adapters/shared/mssql.rb +185 -10
  14. data/lib/sequel/adapters/shared/mysql.rb +9 -9
  15. data/lib/sequel/adapters/shared/sqlite.rb +45 -47
  16. data/lib/sequel/connection_pool.rb +8 -5
  17. data/lib/sequel/core.rb +2 -8
  18. data/lib/sequel/database.rb +9 -10
  19. data/lib/sequel/database/schema_sql.rb +3 -2
  20. data/lib/sequel/dataset.rb +1 -0
  21. data/lib/sequel/dataset/sql.rb +15 -6
  22. data/lib/sequel/extensions/schema_dumper.rb +7 -7
  23. data/lib/sequel/model/associations.rb +16 -14
  24. data/lib/sequel/model/base.rb +25 -7
  25. data/lib/sequel/plugins/association_proxies.rb +41 -0
  26. data/lib/sequel/plugins/many_through_many.rb +0 -1
  27. data/lib/sequel/sql.rb +8 -11
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/mysql_spec.rb +42 -38
  30. data/spec/adapters/sqlite_spec.rb +0 -4
  31. data/spec/core/database_spec.rb +22 -1
  32. data/spec/core/dataset_spec.rb +37 -12
  33. data/spec/core/expression_filters_spec.rb +5 -0
  34. data/spec/core/schema_spec.rb +15 -8
  35. data/spec/extensions/association_proxies_spec.rb +47 -0
  36. data/spec/extensions/caching_spec.rb +2 -2
  37. data/spec/extensions/hook_class_methods_spec.rb +6 -6
  38. data/spec/extensions/many_through_many_spec.rb +13 -0
  39. data/spec/extensions/schema_dumper_spec.rb +12 -4
  40. data/spec/extensions/validation_class_methods_spec.rb +3 -3
  41. data/spec/integration/dataset_test.rb +47 -17
  42. data/spec/integration/prepared_statement_test.rb +5 -5
  43. data/spec/integration/schema_test.rb +111 -34
  44. data/spec/model/associations_spec.rb +128 -11
  45. data/spec/model/hooks_spec.rb +7 -6
  46. data/spec/model/model_spec.rb +54 -4
  47. data/spec/model/record_spec.rb +2 -3
  48. data/spec/model/validations_spec.rb +4 -4
  49. metadata +109 -101
  50. data/spec/adapters/ado_spec.rb +0 -93
@@ -27,12 +27,23 @@ module Sequel
27
27
  def alter_table_sql(table, op)
28
28
  case op[:op]
29
29
  when :add_column
30
- if op.delete(:primary_key)
31
- sql = super(table, op)
32
- [sql, "ALTER TABLE #{quote_schema_table(table)} ADD PRIMARY KEY (#{quote_identifier(op[:name])})"]
30
+ if (pk = op.delete(:primary_key)) || (ref = op.delete(:table))
31
+ sqls = [super(table, op)]
32
+ sqls << "ALTER TABLE #{quote_schema_table(table)} ADD PRIMARY KEY (#{quote_identifier(op[:name])})" if pk
33
+ if ref
34
+ op[:table] = ref
35
+ sqls << "ALTER TABLE #{quote_schema_table(table)} ADD FOREIGN KEY (#{quote_identifier(op[:name])}) #{column_references_sql(op)}"
36
+ end
37
+ sqls
33
38
  else
34
39
  super(table, op)
35
40
  end
41
+ when :rename_column
42
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} RENAME TO #{quote_identifier(op[:new_name])}"
43
+ when :set_column_null
44
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} SET#{' NOT' unless op[:null]} NULL"
45
+ when :set_column_type
46
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(op)}"
36
47
  else
37
48
  super(table, op)
38
49
  end
@@ -0,0 +1,51 @@
1
+ Sequel.require 'adapters/shared/mssql'
2
+
3
+ module Sequel
4
+ module JDBC
5
+ class Database
6
+ # Alias the generic JDBC version so it can be called directly later
7
+ alias jdbc_schema_parse_table schema_parse_table
8
+ end
9
+
10
+ # Database and Dataset instance methods for MSSQL specific
11
+ # support via JDBC.
12
+ module MSSQL
13
+ # Database instance methods for MSSQL databases accessed via JDBC.
14
+ module DatabaseMethods
15
+ include Sequel::MSSQL::DatabaseMethods
16
+
17
+ # Return instance of Sequel::JDBC::MSSQL::Dataset with the given opts.
18
+ def dataset(opts=nil)
19
+ Sequel::JDBC::MSSQL::Dataset.new(self, opts)
20
+ end
21
+
22
+ private
23
+
24
+ # Get the last inserted id using SCOPE_IDENTITY().
25
+ def last_insert_id(conn, opts={})
26
+ stmt = conn.createStatement
27
+ begin
28
+ sql = opts[:prepared] ? 'SELECT @@IDENTITY' : 'SELECT SCOPE_IDENTITY()'
29
+ log_info(sql)
30
+ rs = stmt.executeQuery(sql)
31
+ rs.next
32
+ rs.getInt(1)
33
+ ensure
34
+ stmt.close
35
+ end
36
+ end
37
+
38
+ # Call the generic JDBC version instead of MSSQL version,
39
+ # since the JDBC version handles primary keys.
40
+ def schema_parse_table(table, opts={})
41
+ jdbc_schema_parse_table(table, opts)
42
+ end
43
+ end
44
+
45
+ # Dataset class for MSSQL datasets accessed via JDBC.
46
+ class Dataset < JDBC::Dataset
47
+ include Sequel::MSSQL::DatasetMethods
48
+ end
49
+ end
50
+ end
51
+ end
@@ -7,31 +7,40 @@ module Sequel
7
7
  # A class level convert_invalid_date_time accessor exists if
8
8
  # the native adapter is used. If set to nil or :nil, the adapter treats dates
9
9
  # like 0000-00-00 and times like 838:00:00 as nil values. If set to :string,
10
- # it returns the strings as is. If is false by default, which means that
10
+ # it returns the strings as is. It is false by default, which means that
11
11
  # invalid dates and times will raise errors.
12
+ #
13
+ # Sequel::MySQL.convert_invalid_date_time = true
14
+ #
15
+ # Sequel converts the column type tinyint(1) to a boolean by default when
16
+ # using the native MySQL adapter. You can turn off the conversion to use
17
+ # tinyint as an integer:
18
+ #
19
+ # Sequel.convert_tinyint_to_bool = false
12
20
  module MySQL
13
21
  # Mapping of type numbers to conversion procs
14
22
  MYSQL_TYPES = {}
15
23
 
16
24
  # Use only a single proc for each type to save on memory
17
25
  MYSQL_TYPE_PROCS = {
18
- [0, 246] => lambda{|v| BigDecimal.new(v)}, # decimal
19
- [1] => lambda{|v| Sequel.convert_tinyint_to_bool ? v.to_i != 0 : v.to_i}, # tinyint
20
- [2, 3, 8, 9, 13, 247, 248] => lambda{|v| v.to_i}, # integer
21
- [4, 5] => lambda{|v| v.to_f}, # float
22
- [10, 14] => lambda{|v| convert_date_time(:string_to_date, v)}, # date
23
- [7, 12] => lambda{|v| convert_date_time(:string_to_datetime, v)}, # datetime
24
- [11] => lambda{|v| convert_date_time(:string_to_time, v)}, # time
25
- [249, 250, 251, 252] => lambda{|v| Sequel::SQL::Blob.new(v)} # blob
26
+ [0, 246] => lambda{|v| BigDecimal.new(v)}, # decimal
27
+ [1] => lambda{|v| convert_tinyint_to_bool ? v.to_i != 0 : v.to_i}, # tinyint
28
+ [2, 3, 8, 9, 13, 247, 248] => lambda{|v| v.to_i}, # integer
29
+ [4, 5] => lambda{|v| v.to_f}, # float
30
+ [10, 14] => lambda{|v| convert_date_time(:string_to_date, v)}, # date
31
+ [7, 12] => lambda{|v| convert_date_time(:string_to_datetime, v)}, # datetime
32
+ [11] => lambda{|v| convert_date_time(:string_to_time, v)}, # time
33
+ [249, 250, 251, 252] => lambda{|v| Sequel::SQL::Blob.new(v)} # blob
26
34
  }
27
35
  MYSQL_TYPE_PROCS.each do |k,v|
28
36
  k.each{|n| MYSQL_TYPES[n] = v}
29
37
  end
30
38
 
31
39
  @convert_invalid_date_time = false
40
+ @convert_tinyint_to_bool = true
32
41
 
33
42
  class << self
34
- attr_accessor :convert_invalid_date_time
43
+ attr_accessor :convert_invalid_date_time, :convert_tinyint_to_bool
35
44
  end
36
45
 
37
46
  # If convert_invalid_date_time is nil, :nil, or :string and
@@ -149,8 +158,10 @@ module Sequel
149
158
  yield r if r
150
159
  if conn.respond_to?(:next_result) && conn.next_result
151
160
  loop do
152
- r.free
153
- r = nil
161
+ if r
162
+ r.free
163
+ r = nil
164
+ end
154
165
  begin
155
166
  r = conn.use_result
156
167
  rescue Mysql::Error
@@ -222,6 +233,11 @@ module Sequel
222
233
  _execute(conn, "EXECUTE #{ps_name}#{" USING #{(1..i).map{|j| "@sequel_arg_#{j}"}.join(', ')}" unless i == 0}", opts, &block)
223
234
  end
224
235
  end
236
+
237
+ # Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
238
+ def schema_column_type(db_type)
239
+ Sequel::MySQL.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
240
+ end
225
241
  end
226
242
 
227
243
  # Dataset class for MySQL datasets accessed via the native driver.
@@ -12,8 +12,8 @@ module Sequel
12
12
  super(opts)
13
13
  case opts[:db_type]
14
14
  when 'mssql'
15
- Sequel.require 'adapters/shared/mssql'
16
- extend Sequel::MSSQL::DatabaseMethods
15
+ Sequel.require 'adapters/odbc/mssql'
16
+ extend Sequel::ODBC::MSSQL::DatabaseMethods
17
17
  when 'progress'
18
18
  Sequel.require 'adapters/shared/progress'
19
19
  extend Sequel::Progress::DatabaseMethods
@@ -50,6 +50,8 @@ module Sequel
50
50
  begin
51
51
  r = conn.run(sql)
52
52
  yield(r) if block_given?
53
+ rescue ::ODBC::Error => e
54
+ raise_error(e)
53
55
  ensure
54
56
  r.drop if r
55
57
  end
@@ -59,12 +61,22 @@ module Sequel
59
61
 
60
62
  def execute_dui(sql, opts={})
61
63
  log_info(sql)
62
- synchronize(opts[:server]){|conn| conn.do(sql)}
64
+ synchronize(opts[:server]) do |conn|
65
+ begin
66
+ conn.do(sql)
67
+ rescue ::ODBC::Error => e
68
+ raise_error(e)
69
+ end
70
+ end
63
71
  end
64
- alias_method :do, :execute_dui
72
+ alias do execute_dui
65
73
 
66
74
  private
67
75
 
76
+ def connection_pool_default_options
77
+ super.merge(:pool_convert_exceptions=>false)
78
+ end
79
+
68
80
  def connection_execute_method
69
81
  :do
70
82
  end
@@ -78,27 +90,34 @@ module Sequel
78
90
  BOOL_TRUE = '1'.freeze
79
91
  BOOL_FALSE = '0'.freeze
80
92
  ODBC_TIMESTAMP_FORMAT = "{ts '%Y-%m-%d %H:%M:%S'}".freeze
81
- ODBC_TIMESTAMP_AFTER_SECONDS =
82
- ODBC_TIMESTAMP_FORMAT.index( '%S' ).succ - ODBC_TIMESTAMP_FORMAT.length
93
+ ODBC_TIMESTAMP_FORMAT_USEC = "{ts '%Y-%m-%d %H:%M:%S.%%i'}".freeze
83
94
  ODBC_DATE_FORMAT = "{d '%Y-%m-%d'}".freeze
84
- UNTITLED_COLUMN = 'untitled_%d'.freeze
85
95
 
86
96
  def fetch_rows(sql, &block)
87
97
  execute(sql) do |s|
88
- untitled_count = 0
89
- @columns = s.columns(true).map do |c|
90
- if (n = c.name).empty?
91
- n = UNTITLED_COLUMN % (untitled_count += 1)
98
+ i = -1
99
+ cols = s.columns(true).map{|c| [output_identifier(c.name), i+=1]}
100
+ @columns = cols.map{|c| c.at(0)}
101
+ if rows = s.fetch_all
102
+ rows.each do |row|
103
+ hash = {}
104
+ cols.each{|n,i| hash[n] = convert_odbc_value(row[i])}
105
+ yield hash
92
106
  end
93
- output_identifier(n)
94
107
  end
95
- rows = s.fetch_all
96
- rows.each {|row| yield hash_row(row)} if rows
97
108
  end
98
109
  self
99
110
  end
100
111
 
101
112
  private
113
+
114
+ def _literal_datetime(v, usec)
115
+ if usec >= 1000
116
+ v.strftime(ODBC_TIMESTAMP_FORMAT_USEC) % (usec.to_f/1000).round
117
+ else
118
+ v.strftime(ODBC_TIMESTAMP_FORMAT)
119
+ end
120
+ end
102
121
 
103
122
  def convert_odbc_value(v)
104
123
  # When fetching a result set, the Ruby ODBC driver converts all ODBC
@@ -119,24 +138,13 @@ module Sequel
119
138
  v
120
139
  end
121
140
  end
122
-
123
- def hash_row(row)
124
- hash = {}
125
- row.each_with_index do |v, idx|
126
- hash[@columns[idx]] = convert_odbc_value(v)
127
- end
128
- hash
129
- end
130
-
141
+
131
142
  def literal_date(v)
132
143
  v.strftime(ODBC_DATE_FORMAT)
133
144
  end
134
145
 
135
146
  def literal_datetime(v)
136
- formatted = v.strftime(ODBC_TIMESTAMP_FORMAT)
137
- usec = v.sec_fraction * 86400000000
138
- formatted.insert(ODBC_TIMESTAMP_AFTER_SECONDS, ".#{(usec.to_f/1000).round}") if usec >= 1000
139
- formatted
147
+ _literal_datetime(v, v.sec_fraction * 86400000000)
140
148
  end
141
149
 
142
150
  def literal_false
@@ -148,9 +156,7 @@ module Sequel
148
156
  end
149
157
 
150
158
  def literal_time(v)
151
- formatted = v.strftime(ODBC_TIMESTAMP_FORMAT)
152
- formatted.insert(ODBC_TIMESTAMP_AFTER_SECONDS, ".#{(v.usec.to_f/1000).round}") if usec >= 1000
153
- formatted
159
+ _literal_datetime(v, v.usec)
154
160
  end
155
161
  end
156
162
  end
@@ -0,0 +1,44 @@
1
+ Sequel.require 'adapters/shared/mssql'
2
+
3
+ module Sequel
4
+ module ODBC
5
+ # Database and Dataset instance methods for MSSQL specific
6
+ # support via ODBC.
7
+ module MSSQL
8
+ module DatabaseMethods
9
+ include Sequel::MSSQL::DatabaseMethods
10
+ LAST_INSERT_ID_SQL='SELECT SCOPE_IDENTITY()'
11
+
12
+ # Return an instance of Sequel::ODBC::MSSQL::Dataset with the given opts.
13
+ def dataset(opts=nil)
14
+ Sequel::ODBC::MSSQL::Dataset.new(self, opts)
15
+ end
16
+
17
+ # Return the last inserted identity value.
18
+ def execute_insert(sql, opts={})
19
+ log_info(sql)
20
+ synchronize(opts[:server]) do |conn|
21
+ begin
22
+ conn.do(sql)
23
+ log_info(LAST_INSERT_ID_SQL)
24
+ begin
25
+ s = conn.run(LAST_INSERT_ID_SQL)
26
+ if (rows = s.fetch_all) and (row = rows.first)
27
+ Integer(row.first)
28
+ end
29
+ ensure
30
+ s.drop if s
31
+ end
32
+ rescue ::ODBC::Error => e
33
+ raise_error(e)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ class Dataset < ODBC::Dataset
40
+ include Sequel::MSSQL::DatasetMethods
41
+ end
42
+ end
43
+ end
44
+ end
@@ -5,6 +5,8 @@ module Sequel
5
5
  SQL_BEGIN = "BEGIN TRANSACTION".freeze
6
6
  SQL_COMMIT = "COMMIT TRANSACTION".freeze
7
7
  SQL_ROLLBACK = "ROLLBACK TRANSACTION".freeze
8
+ SQL_ROLLBACK_TO_SAVEPOINT = 'ROLLBACK TRANSACTION autopoint_%d'.freeze
9
+ SQL_SAVEPOINT = 'SAVE TRANSACTION autopoint_%d'.freeze
8
10
  TEMPORARY = "#".freeze
9
11
 
10
12
  # Microsoft SQL Server uses the :mssql type.
@@ -12,42 +14,142 @@ module Sequel
12
14
  :mssql
13
15
  end
14
16
 
15
- def dataset(opts = nil)
16
- ds = super
17
- ds.extend(DatasetMethods)
18
- ds
17
+ # Microsoft SQL Server supports using the INFORMATION_SCHEMA to get
18
+ # information on tables.
19
+ def tables(opts={})
20
+ m = output_identifier_meth
21
+ metadata_dataset.from(:information_schema__tables___t).
22
+ select(:table_name).
23
+ filter(:table_type=>'BASE TABLE', :table_schema=>(opts[:schema]||default_schema||'dbo').to_s).
24
+ map{|x| m.call(x[:table_name])}
25
+ end
26
+
27
+ # MSSQL supports savepoints, though it doesn't support committing/releasing them savepoint
28
+ def supports_savepoints?
29
+ true
19
30
  end
20
31
 
21
32
  private
22
-
33
+
34
+ # MSSQL uses the IDENTITY(1,1) column for autoincrementing columns.
23
35
  def auto_increment_sql
24
36
  AUTO_INCREMENT
25
37
  end
38
+
39
+ # MSSQL specific syntax for altering tables.
40
+ def alter_table_sql(table, op)
41
+ case op[:op]
42
+ when :add_column
43
+ "ALTER TABLE #{quote_schema_table(table)} ADD #{column_definition_sql(op)}"
44
+ when :rename_column
45
+ "SP_RENAME #{literal("#{quote_schema_table(table)}.#{quote_identifier(op[:name])}")}, #{literal(op[:new_name].to_s)}, 'COLUMN'"
46
+ when :set_column_type
47
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(op)}"
48
+ when :set_column_null
49
+ sch = schema(table).find{|k,v| k.to_s == op[:name].to_s}.last
50
+ type = {:type=>sch[:db_type]}
51
+ type[:size] = sch[:max_chars] if sch[:max_chars]
52
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(type)} #{'NOT ' unless op[:null]}NULL"
53
+ when :set_column_default
54
+ "ALTER TABLE #{quote_schema_table(table)} ADD CONSTRAINT #{quote_identifier("sequel_#{table}_#{op[:name]}_def")} DEFAULT #{literal(op[:default])} FOR #{quote_identifier(op[:name])}"
55
+ else
56
+ super(table, op)
57
+ end
58
+ end
59
+
60
+ # SQL to start a new savepoint
61
+ def begin_savepoint_sql(depth)
62
+ SQL_SAVEPOINT % depth
63
+ end
26
64
 
27
65
  # SQL to BEGIN a transaction.
28
66
  def begin_transaction_sql
29
67
  SQL_BEGIN
30
68
  end
69
+
70
+ # Commit the active transaction on the connection, does not commit/release
71
+ # savepoints.
72
+ def commit_transaction(conn)
73
+ log_connection_execute(conn, commit_transaction_sql) unless Thread.current[:sequel_transaction_depth] > 1
74
+ end
31
75
 
32
76
  # SQL to COMMIT a transaction.
33
77
  def commit_transaction_sql
34
78
  SQL_COMMIT
35
79
  end
36
80
 
81
+ # The SQL to drop an index for the table.
82
+ def drop_index_sql(table, op)
83
+ "DROP INDEX #{quote_identifier(op[:name] || default_index_name(table, op[:columns]))} ON #{quote_schema_table(table)}"
84
+ end
85
+
86
+ # SQL to rollback to a savepoint
87
+ def rollback_savepoint_sql(depth)
88
+ SQL_ROLLBACK_TO_SAVEPOINT % depth
89
+ end
90
+
37
91
  # SQL to ROLLBACK a transaction.
38
92
  def rollback_transaction_sql
39
93
  SQL_ROLLBACK
40
94
  end
95
+
96
+ # MSSQL uses the INFORMATION_SCHEMA to hold column information. This method does
97
+ # not support the parsing of primary key information.
98
+ def schema_parse_table(table_name, opts)
99
+ m = output_identifier_meth
100
+ m2 = input_identifier_meth
101
+ ds = metadata_dataset.from(:information_schema__tables___t).
102
+ join(:information_schema__columns___c, :table_catalog=>:table_catalog,
103
+ :table_schema => :table_schema, :table_name => :table_name).
104
+ select(:column_name___column, :data_type___db_type, :character_maximum_length___max_chars, :column_default___default, :is_nullable___allow_null).
105
+ filter(:c__table_name=>m2.call(table_name.to_s))
106
+ if schema = opts[:schema] || default_schema
107
+ ds.filter!(:table_schema=>schema)
108
+ end
109
+ ds.map do |row|
110
+ row[:allow_null] = row[:allow_null] == 'YES' ? true : false
111
+ row[:default] = nil if blank_object?(row[:default])
112
+ row[:type] = schema_column_type(row[:db_type])
113
+ [m.call(row.delete(:column)), row]
114
+ end
115
+ end
41
116
 
42
117
  # SQL fragment for marking a table as temporary
43
118
  def temporary_table_sql
44
119
  TEMPORARY
45
120
  end
121
+
122
+ # MSSQL has both datetime and timestamp classes, most people are going
123
+ # to want datetime
124
+ def type_literal_generic_datetime(column)
125
+ :datetime
126
+ end
127
+
128
+ # MSSQL has both datetime and timestamp classes, most people are going
129
+ # to want datetime
130
+ def type_literal_generic_time(column)
131
+ column[:only_time] ? :time : :datetime
132
+ end
133
+
134
+ # MSSQL doesn't have a true boolean class, so it uses bit
135
+ def type_literal_generic_trueclass(column)
136
+ :bit
137
+ end
138
+
139
+ # MSSQL uses image type for blobs
140
+ def type_literal_generic_file(column)
141
+ :image
142
+ end
46
143
  end
47
144
 
48
145
  module DatasetMethods
146
+ BOOL_TRUE = '1'.freeze
147
+ BOOL_FALSE = '0'.freeze
49
148
  SELECT_CLAUSE_ORDER = %w'with limit distinct columns from table_options join where group order having compounds'.freeze
50
-
149
+ TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S'".freeze
150
+ WILDCARD = LiteralString.new('*').freeze
151
+
152
+ # MSSQL uses + for string concatenation
51
153
  def complex_expression_sql(op, args)
52
154
  case op
53
155
  when :'||'
@@ -57,10 +159,18 @@ module Sequel
57
159
  end
58
160
  end
59
161
 
162
+ # When returning all rows, if an offset is used, delete the row_number column
163
+ # before yielding the row.
164
+ def each(&block)
165
+ @opts[:offset] ? super{|r| r.delete(row_number_column); yield r} : super(&block)
166
+ end
167
+
168
+ # MSSQL uses the CONTAINS keyword for full text search
60
169
  def full_text_search(cols, terms, opts = {})
61
170
  filter("CONTAINS (#{literal(cols)}, #{literal(terms)})")
62
171
  end
63
172
 
173
+ # MSSQL uses a UNION ALL statement to insert multiple values at once.
64
174
  def multi_insert_sql(columns, values)
65
175
  values = values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")
66
176
  ["#{insert_sql_base}#{source_list(@opts[:from])} (#{identifier_list(columns)}) #{values}"]
@@ -70,34 +180,99 @@ module Sequel
70
180
  def nolock
71
181
  clone(:table_options => "(NOLOCK)")
72
182
  end
73
-
183
+
184
+ # MSSQL uses [] to quote identifiers
74
185
  def quoted_identifier(name)
75
186
  "[#{name}]"
76
187
  end
188
+
189
+ # MSSQL Requires the use of the ROW_NUMBER window function to emulate
190
+ # an offset. This implementation requires MSSQL 2005 or greater (offset
191
+ # can't be emulated well in MSSQL 2000).
192
+ #
193
+ # The implementation is ugly, cloning the current dataset and modifying
194
+ # the clone to add a ROW_NUMBER window function (and some other things),
195
+ # then using the modified clone in a CTE which is selected from.
196
+ #
197
+ # If offset is used, an order must be provided, because the use of ROW_NUMBER
198
+ # requires an order.
199
+ def select_sql
200
+ return super unless o = @opts[:offset]
201
+ raise(Error, 'MSSQL requires an order be provided if using an offset') unless order = @opts[:order]
202
+ dsa1 = dataset_alias(1)
203
+ dsa2 = dataset_alias(2)
204
+ rn = row_number_column
205
+ unlimited.
206
+ unordered.
207
+ from_self(:alias=>dsa2).
208
+ select{[WILDCARD, ROW_NUMBER(:over, :order=>order){}.as(rn)]}.
209
+ from_self(:alias=>dsa1).
210
+ limit(@opts[:limit]).
211
+ where(rn > o).
212
+ select_sql
213
+ end
77
214
 
78
215
  # Microsoft SQL Server does not support INTERSECT or EXCEPT
79
216
  def supports_intersect_except?
80
217
  false
81
218
  end
82
219
 
220
+ # MSSQL does not support IS TRUE
221
+ def supports_is_true?
222
+ false
223
+ end
224
+
83
225
  # MSSQL 2005+ supports window functions
84
226
  def supports_window_functions?
85
227
  true
86
228
  end
87
229
 
88
230
  private
89
-
231
+
232
+ # MSSQL uses a literal hexidecimal number for blob strings
233
+ def literal_blob(v)
234
+ blob = '0x'
235
+ v.each_byte{|x| blob << sprintf('%02x', x)}
236
+ blob
237
+ end
238
+
239
+ # Use unicode string syntax for all strings
90
240
  def literal_string(v)
91
241
  "N#{super}"
92
242
  end
243
+
244
+ # Use MSSQL Timestamp format
245
+ def literal_datetime(v)
246
+ v.strftime(TIMESTAMP_FORMAT)
247
+ end
248
+
249
+ # Use 0 for false on MSSQL
250
+ def literal_false
251
+ BOOL_FALSE
252
+ end
253
+
254
+ # Use MSSQL Timestamp format
255
+ def literal_time(v)
256
+ v.strftime(TIMESTAMP_FORMAT)
257
+ end
258
+
259
+ # Use 1 for true on MSSQL
260
+ def literal_true
261
+ BOOL_TRUE
262
+ end
263
+
264
+ # The alias to use for the row_number column when emulating OFFSET
265
+ def row_number_column
266
+ :x_sequel_row_number_x
267
+ end
93
268
 
269
+ # MSSQL adds the limit before the columns
94
270
  def select_clause_order
95
271
  SELECT_CLAUSE_ORDER
96
272
  end
97
273
 
98
- # MSSQL uses TOP for limit, with no offset support
274
+ # MSSQL uses TOP for limit
99
275
  def select_limit_sql(sql)
100
- raise(Error, "OFFSET not supported") if @opts[:offset]
101
276
  sql << " TOP #{@opts[:limit]}" if @opts[:limit]
102
277
  end
103
278