sequel 3.2.0 → 3.3.0

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