sequel 3.13.0 → 3.14.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 (60) hide show
  1. data/CHANGELOG +36 -0
  2. data/doc/release_notes/3.14.0.txt +118 -0
  3. data/lib/sequel/adapters/oracle.rb +7 -2
  4. data/lib/sequel/adapters/shared/mssql.rb +9 -3
  5. data/lib/sequel/connection_pool/sharded_threaded.rb +1 -1
  6. data/lib/sequel/connection_pool/threaded.rb +3 -3
  7. data/lib/sequel/database/connecting.rb +47 -11
  8. data/lib/sequel/database/dataset.rb +17 -6
  9. data/lib/sequel/database/dataset_defaults.rb +15 -3
  10. data/lib/sequel/database/logging.rb +4 -3
  11. data/lib/sequel/database/misc.rb +33 -21
  12. data/lib/sequel/database/query.rb +61 -22
  13. data/lib/sequel/database/schema_generator.rb +108 -45
  14. data/lib/sequel/database/schema_methods.rb +8 -5
  15. data/lib/sequel/dataset/actions.rb +194 -45
  16. data/lib/sequel/dataset/features.rb +1 -1
  17. data/lib/sequel/dataset/graph.rb +51 -43
  18. data/lib/sequel/dataset/misc.rb +29 -5
  19. data/lib/sequel/dataset/mutation.rb +0 -1
  20. data/lib/sequel/dataset/prepared_statements.rb +14 -2
  21. data/lib/sequel/dataset/query.rb +268 -125
  22. data/lib/sequel/dataset/sql.rb +33 -44
  23. data/lib/sequel/extensions/migration.rb +3 -2
  24. data/lib/sequel/extensions/pagination.rb +1 -1
  25. data/lib/sequel/model/associations.rb +89 -87
  26. data/lib/sequel/model/base.rb +386 -109
  27. data/lib/sequel/model/errors.rb +15 -1
  28. data/lib/sequel/model/exceptions.rb +3 -3
  29. data/lib/sequel/model/inflections.rb +2 -2
  30. data/lib/sequel/model/plugins.rb +9 -5
  31. data/lib/sequel/plugins/rcte_tree.rb +43 -15
  32. data/lib/sequel/plugins/schema.rb +6 -5
  33. data/lib/sequel/plugins/serialization.rb +1 -1
  34. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  35. data/lib/sequel/plugins/tree.rb +33 -1
  36. data/lib/sequel/timezones.rb +16 -10
  37. data/lib/sequel/version.rb +1 -1
  38. data/spec/adapters/mssql_spec.rb +36 -2
  39. data/spec/adapters/mysql_spec.rb +4 -4
  40. data/spec/adapters/postgres_spec.rb +1 -1
  41. data/spec/adapters/spec_helper.rb +2 -2
  42. data/spec/core/database_spec.rb +8 -1
  43. data/spec/core/dataset_spec.rb +36 -1
  44. data/spec/extensions/pagination_spec.rb +1 -1
  45. data/spec/extensions/rcte_tree_spec.rb +40 -8
  46. data/spec/extensions/schema_spec.rb +5 -0
  47. data/spec/extensions/serialization_spec.rb +4 -4
  48. data/spec/extensions/single_table_inheritance_spec.rb +7 -0
  49. data/spec/extensions/tree_spec.rb +36 -0
  50. data/spec/integration/dataset_test.rb +19 -0
  51. data/spec/integration/prepared_statement_test.rb +2 -2
  52. data/spec/integration/schema_test.rb +1 -1
  53. data/spec/integration/spec_helper.rb +4 -4
  54. data/spec/integration/timezone_test.rb +27 -21
  55. data/spec/model/associations_spec.rb +5 -5
  56. data/spec/model/dataset_methods_spec.rb +13 -0
  57. data/spec/model/hooks_spec.rb +31 -0
  58. data/spec/model/record_spec.rb +24 -7
  59. data/spec/model/validations_spec.rb +9 -4
  60. metadata +6 -4
@@ -5,17 +5,24 @@ module Sequel
5
5
  class Errors < ::Hash
6
6
  ATTRIBUTE_JOINER = ' and '.freeze
7
7
 
8
- # Assign an array of messages for each attribute on access
8
+ # Assign an array of messages for each attribute on access.
9
+ # Using this message is discouraged in new code, use +add+
10
+ # to add new error messages, and +on+ to check existing
11
+ # error messages.
9
12
  def [](k)
10
13
  has_key?(k) ? super : (self[k] = [])
11
14
  end
12
15
 
13
16
  # Adds an error for the given attribute.
17
+ #
18
+ # errors.add(:name, 'is not valid') if name == 'invalid'
14
19
  def add(att, msg)
15
20
  self[att] << msg
16
21
  end
17
22
 
18
23
  # Return the total number of error messages.
24
+ #
25
+ # errors.count # => 3
19
26
  def count
20
27
  values.inject(0){|m, v| m + v.length}
21
28
  end
@@ -26,6 +33,10 @@ module Sequel
26
33
  end
27
34
 
28
35
  # Returns an array of fully-formatted error messages.
36
+ #
37
+ # errors.full_messages
38
+ # # => ['name is not valid',
39
+ # # 'hometown is not at least 2 letters']
29
40
  def full_messages
30
41
  inject([]) do |m, kv|
31
42
  att, errors = *kv
@@ -36,6 +47,9 @@ module Sequel
36
47
 
37
48
  # Returns the array of errors for the given attribute, or nil
38
49
  # if there are no errors for the attribute.
50
+ #
51
+ # errors.on(:name) # => ['name is not valid']
52
+ # errors.on(:id) # => nil
39
53
  def on(att)
40
54
  if v = fetch(att, nil) and !v.empty?
41
55
  v
@@ -1,12 +1,12 @@
1
1
  module Sequel
2
- # Exception class raised when raise_on_save_failure is set and a before hook returns false
2
+ # Exception class raised when +raise_on_save_failure+ is set and a before hook returns false
3
3
  class BeforeHookFailed < Error; end
4
4
 
5
- # Exception class raised when require_modification is set and an UPDATE or DELETE statement to modify the dataset doesn't
5
+ # Exception class raised when +require_modification+ is set and an UPDATE or DELETE statement to modify the dataset doesn't
6
6
  # modify a single row.
7
7
  class NoExistingObject < Error; end
8
8
 
9
- # Exception class raised when raise_on_save_failure is set and validation fails
9
+ # Exception class raised when +raise_on_save_failure+ is set and validation fails
10
10
  class ValidationFailed < Error
11
11
  def initialize(errors)
12
12
  if errors.respond_to?(:full_messages)
@@ -39,10 +39,10 @@ module Sequel
39
39
  @plurals, @singulars, @uncountables = [], [], []
40
40
 
41
41
  class << self
42
- # Array of 2 element arrays, first containing a regex, and the second containing a substitution pattern, used for plurization.
42
+ # Array of two element arrays, first containing a regex, and the second containing a substitution pattern, used for plurization.
43
43
  attr_reader :plurals
44
44
 
45
- # Array of 2 element arrays, first containing a regex, and the second containing a substitution pattern, used for singularization.
45
+ # Array of two element arrays, first containing a regex, and the second containing a substitution pattern, used for singularization.
46
46
  attr_reader :singulars
47
47
 
48
48
  # Array of strings for words were the singular form is the same as the plural form
@@ -6,7 +6,8 @@ module Sequel
6
6
  # * A singleton method named apply, which takes a model,
7
7
  # additional arguments, and an optional block. This is called
8
8
  # the first time the plugin is loaded for this model (unless it was
9
- # already loaded by an ancestor class), with the arguments
9
+ # already loaded by an ancestor class), before including/extending
10
+ # any modules, with the arguments
10
11
  # and block provided to the call to Model.plugin.
11
12
  # * A module inside the plugin module named InstanceMethods,
12
13
  # which will be included in the model class.
@@ -16,7 +17,8 @@ module Sequel
16
17
  # which will extend the model's dataset.
17
18
  # * A singleton method named configure, which takes a model,
18
19
  # additional arguments, and an optional block. This is called
19
- # every time the Model.plugin method is called.
20
+ # every time the Model.plugin method is called, after including/extending
21
+ # any modules.
20
22
  module Plugins
21
23
  end
22
24
 
@@ -27,7 +29,6 @@ module Sequel
27
29
  # sequel_#{plugin}, and then attempt to load the module using a
28
30
  # the camelized plugin name under Sequel::Plugins.
29
31
  def self.plugin(plugin, *args, &blk)
30
- arg = args.first
31
32
  m = plugin.is_a?(Module) ? plugin : plugin_module(plugin)
32
33
  unless @plugins.include?(m)
33
34
  @plugins << m
@@ -45,13 +46,16 @@ module Sequel
45
46
  end
46
47
 
47
48
  module ClassMethods
48
- # Array of plugins loaded by this class
49
+ # Array of plugin modules loaded by this class
50
+ #
51
+ # Sequel::Model.plugins
52
+ # # => [Sequel::Model, Sequel::Model::Associations]
49
53
  attr_reader :plugins
50
54
 
51
55
  private
52
56
 
53
57
  # Returns the module for the specified plugin. If the module is not
54
- # defined, the corresponding plugin gem is automatically loaded.
58
+ # defined, the corresponding plugin required.
55
59
  def plugin_module(plugin)
56
60
  module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
57
61
  if !Sequel::Plugins.const_defined?(module_name) ||
@@ -32,9 +32,7 @@ module Sequel
32
32
  #
33
33
  # = Usage
34
34
  #
35
- # The rcte_tree plugin is unlike most plugins in that it doesn't add any class,
36
- # instance, or dataset modules. It only has a single apply method, which
37
- # adds four associations to the model: parent, children, ancestors, and
35
+ # The rcte_tree plugin adds four associations to the model: parent, children, ancestors, and
38
36
  # descendants. Both the parent and children are fairly standard many_to_one
39
37
  # and one_to_many associations, respectively. However, the ancestors and
40
38
  # descendants associations are special. Both the ancestors and descendants
@@ -118,9 +116,17 @@ module Sequel
118
116
  a[:read_only] = true unless a.has_key?(:read_only)
119
117
  a[:eager_loader_key] = key
120
118
  a[:dataset] ||= proc do
121
- model.from(t).
122
- with_recursive(t, model.filter(prkey=>send(key)),
123
- model.join(t, key=>prkey).
119
+ base_ds = model.filter(prkey=>send(key))
120
+ recursive_ds = model.join(t, key=>prkey)
121
+ if c = a[:conditions]
122
+ (base_ds, recursive_ds) = [base_ds, recursive_ds].collect do |ds|
123
+ (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
124
+ end
125
+ end
126
+ table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
127
+ model.from(t => table_alias).
128
+ with_recursive(t, base_ds,
129
+ recursive_ds.
124
130
  select(c_all))
125
131
  end
126
132
  aal = Array(a[:after_load])
@@ -156,12 +162,20 @@ module Sequel
156
162
  obj.associations[parent] = nil
157
163
  end
158
164
  r = model.association_reflection(ancestors)
165
+ base_case = model.filter(prkey=>id_map.keys).
166
+ select(SQL::AliasedExpression.new(prkey, ka), c_all)
167
+ recursive_case = model.join(t, key=>prkey).
168
+ select(SQL::QualifiedIdentifier.new(t, ka), c_all)
169
+ if c = r[:conditions]
170
+ (base_case, recursive_case) = [base_case, recursive_case].collect do |ds|
171
+ (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
172
+ end
173
+ end
174
+ table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
159
175
  model.eager_loading_dataset(r,
160
- model.from(t).
161
- with_recursive(t, model.filter(prkey=>id_map.keys).
162
- select(SQL::AliasedExpression.new(prkey, ka), c_all),
163
- model.join(t, key=>prkey).
164
- select(SQL::QualifiedIdentifier.new(t, ka), c_all)),
176
+ model.from(t => table_alias).
177
+ with_recursive(t, base_case,
178
+ recursive_case),
165
179
  r.select,
166
180
  eo[:associations], eo).all do |obj|
167
181
  opk = obj[prkey]
@@ -197,9 +211,17 @@ module Sequel
197
211
  d[:read_only] = true unless d.has_key?(:read_only)
198
212
  la = d[:level_alias] ||= :x_level_x
199
213
  d[:dataset] ||= proc do
200
- model.from(t).
201
- with_recursive(t, model.filter(key=>send(prkey)),
202
- model.join(t, prkey=>key).
214
+ base_ds = model.filter(key=>send(prkey))
215
+ recursive_ds = model.join(t, prkey=>key)
216
+ if c = d[:conditions]
217
+ (base_ds, recursive_ds) = [base_ds, recursive_ds].collect do |ds|
218
+ (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
219
+ end
220
+ end
221
+ table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
222
+ model.from(t => table_alias).
223
+ with_recursive(t, base_ds,
224
+ recursive_ds.
203
225
  select(SQL::ColumnAll.new(model.table_name)))
204
226
  end
205
227
  dal = Array(d[:after_load])
@@ -238,6 +260,11 @@ module Sequel
238
260
  select(SQL::AliasedExpression.new(key, ka), c_all)
239
261
  recursive_case = model.join(t, prkey=>key).
240
262
  select(SQL::QualifiedIdentifier.new(t, ka), c_all)
263
+ if c = r[:conditions]
264
+ (base_case, recursive_case) = [base_case, recursive_case].collect do |ds|
265
+ (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
266
+ end
267
+ end
241
268
  if associations.is_a?(Integer)
242
269
  level = associations
243
270
  no_cache_level = level - 1
@@ -245,8 +272,9 @@ module Sequel
245
272
  base_case = base_case.select_more(SQL::AliasedExpression.new(0, la))
246
273
  recursive_case = recursive_case.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, la) + 1, la)).filter(SQL::QualifiedIdentifier.new(t, la) < level - 1)
247
274
  end
275
+ table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
248
276
  model.eager_loading_dataset(r,
249
- model.from(t).with_recursive(t, base_case, recursive_case),
277
+ model.from(t => table_alias).with_recursive(t, base_case, recursive_case),
250
278
  r.select,
251
279
  associations, eo).all do |obj|
252
280
  if level
@@ -19,7 +19,8 @@ module Sequel
19
19
  module Schema
20
20
  module ClassMethods
21
21
  # Creates table, using the column information from set_schema.
22
- def create_table
22
+ def create_table(*args, &block)
23
+ set_schema(*args, &block) if block_given?
23
24
  db.create_table(table_name, :generator=>@schema)
24
25
  @db_schema = get_db_schema(true)
25
26
  columns
@@ -27,14 +28,14 @@ module Sequel
27
28
 
28
29
  # Drops the table if it exists and then runs create_table. Should probably
29
30
  # not be used except in testing.
30
- def create_table!
31
+ def create_table!(*args, &block)
31
32
  drop_table rescue nil
32
- create_table
33
+ create_table(*args, &block)
33
34
  end
34
35
 
35
36
  # Creates the table unless the table already exists
36
- def create_table?
37
- create_table unless table_exists?
37
+ def create_table?(*args, &block)
38
+ create_table(*args, &block) unless table_exists?
38
39
  end
39
40
 
40
41
  # Drops table.
@@ -153,7 +153,7 @@ module Sequel
153
153
  when :yaml
154
154
  v.to_yaml
155
155
  when :json
156
- JSON.generate v
156
+ v.to_json
157
157
  else
158
158
  raise Error, "Bad serialization format (#{model.serialization_map[column].inspect}) for column #{column.inspect}"
159
159
  end
@@ -157,7 +157,7 @@ module Sequel
157
157
  module InstanceMethods
158
158
  # Set the sti_key column based on the sti_key_map.
159
159
  def before_create
160
- send("#{model.sti_key}=", model.sti_key_map[model]) unless send(model.sti_key)
160
+ send("#{model.sti_key}=", model.sti_key_map[model]) unless self[model.sti_key]
161
161
  super
162
162
  end
163
163
  end
@@ -8,6 +8,9 @@ module Sequel
8
8
  #
9
9
  # Optionally, a column to control order of nodes returned can be specified
10
10
  # by passing column name via :order.
11
+ #
12
+ # If you pass true for the :single_root option, the class will ensure there is
13
+ # only ever one root in the tree.
11
14
  #
12
15
  # Examples:
13
16
  #
@@ -40,6 +43,8 @@ module Sequel
40
43
  chi = opts.merge(opts.fetch(:children, {}))
41
44
  children = chi.fetch(:name, :children)
42
45
  model.one_to_many children, chi
46
+
47
+ model.plugin SingleRoot if opts[:single_root]
43
48
  end
44
49
 
45
50
  module ClassMethods
@@ -88,7 +93,7 @@ module Sequel
88
93
  #
89
94
  # subchild1.ancestors # => [child1, root]
90
95
  def descendants
91
- nodes = self.children.dup
96
+ nodes = children.dup
92
97
  nodes.each{|child| nodes.concat(child.descendants)}
93
98
  nodes
94
99
  end
@@ -99,6 +104,11 @@ module Sequel
99
104
  ancestors.last || self
100
105
  end
101
106
 
107
+ # Returns true if this is a root node, false otherwise.
108
+ def root?
109
+ !new? && self[model.parent_column].nil?
110
+ end
111
+
102
112
  # Returns all siblings and a reference to the current node.
103
113
  #
104
114
  # subchild1.self_and_siblings # => [subchild1, subchild2]
@@ -113,6 +123,28 @@ module Sequel
113
123
  self_and_siblings - [self]
114
124
  end
115
125
  end
126
+
127
+ # Plugin included when :single_root option is passed
128
+ module SingleRoot
129
+ module ClassMethods
130
+ # Returns the single root node.
131
+ def root
132
+ roots_dataset.first
133
+ end
134
+ end
135
+
136
+ module InstanceMethods
137
+ # Hook that prevents a second root from being created.
138
+ def before_save
139
+ if self[model.parent_column].nil? && (root = model.root) && pk != root.pk
140
+ raise TreeMultipleRootError, "there is already a root #{model.name} defined"
141
+ end
142
+ super
143
+ end
144
+ end
145
+ end
146
+
147
+ class TreeMultipleRootError < Error; end
116
148
  end
117
149
  end
118
150
  end
@@ -1,10 +1,4 @@
1
1
  module Sequel
2
- # The offset of the current time zone from UTC, in seconds.
3
- LOCAL_DATETIME_OFFSET_SECS = Time.now.utc_offset
4
-
5
- # The offset of the current time zone from UTC, as a fraction of a day.
6
- LOCAL_DATETIME_OFFSET = respond_to?(:Rational, true) ? Rational(LOCAL_DATETIME_OFFSET_SECS, 60*60*24) : LOCAL_DATETIME_OFFSET_SECS/60/60/24.0
7
-
8
2
  @application_timezone = nil
9
3
  @database_timezone = nil
10
4
  @typecast_timezone = nil
@@ -75,7 +69,8 @@ module Sequel
75
69
  when :utc, nil
76
70
  v # DateTime assumes UTC if no offset is given
77
71
  when :local
78
- v.new_offset(LOCAL_DATETIME_OFFSET) - LOCAL_DATETIME_OFFSET
72
+ offset = local_offset_for_datetime(v)
73
+ v.new_offset(offset) - offset
79
74
  else
80
75
  convert_input_datetime_other(v, input_timezone)
81
76
  end
@@ -103,7 +98,7 @@ module Sequel
103
98
  v2 = convert_input_datetime_no_offset(v2, input_timezone)
104
99
  else
105
100
  # Time assumes local time if no offset is given
106
- v2 = v2.getutc + LOCAL_DATETIME_OFFSET_SECS if input_timezone == :utc
101
+ v2 = v2.getutc + v2.utc_offset if input_timezone == :utc
107
102
  end
108
103
  v2
109
104
  end
@@ -132,7 +127,7 @@ module Sequel
132
127
  raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}"
133
128
  end
134
129
  end
135
-
130
+
136
131
  # Convert the given +DateTime+ to the given output_timezone that is not supported
137
132
  # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
138
133
  # Can be overridden in extensions.
@@ -148,7 +143,7 @@ module Sequel
148
143
  when :utc
149
144
  v.new_offset(0)
150
145
  when :local
151
- v.new_offset(LOCAL_DATETIME_OFFSET)
146
+ v.new_offset(local_offset_for_datetime(v))
152
147
  else
153
148
  convert_output_datetime_other(v, output_timezone)
154
149
  end
@@ -178,6 +173,17 @@ module Sequel
178
173
  def convert_timezone_setter_arg(tz)
179
174
  tz
180
175
  end
176
+
177
+ # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included.
178
+ def local_offset_for_datetime(dt)
179
+ time_offset_to_datetime_offset Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec).utc_offset
180
+ end
181
+
182
+ # Caches offset conversions to avoid excess Rational math.
183
+ def time_offset_to_datetime_offset(offset_secs)
184
+ @local_offsets ||= {}
185
+ @local_offsets[offset_secs] ||= respond_to?(:Rational, true) ? Rational(offset_secs, 60*60*24) : offset_secs/60/60/24.0
186
+ end
181
187
  end
182
188
 
183
189
  extend Timezones
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 13
6
+ MINOR = 14
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -63,8 +63,9 @@ context "MSSQL Dataset#join_table" do
63
63
  specify "should emulate the USING clause with ON" do
64
64
  MSSQL_DB[:items].join(:categories, [:id]).sql.should ==
65
65
  'SELECT * FROM ITEMS INNER JOIN CATEGORIES ON (CATEGORIES.ID = ITEMS.ID)'
66
- MSSQL_DB[:items].join(:categories, [:id1, :id2]).sql.should ==
67
- 'SELECT * FROM ITEMS INNER JOIN CATEGORIES ON ((CATEGORIES.ID1 = ITEMS.ID1) AND (CATEGORIES.ID2 = ITEMS.ID2))'
66
+ ['SELECT * FROM ITEMS INNER JOIN CATEGORIES ON ((CATEGORIES.ID1 = ITEMS.ID1) AND (CATEGORIES.ID2 = ITEMS.ID2))',
67
+ 'SELECT * FROM ITEMS INNER JOIN CATEGORIES ON ((CATEGORIES.ID2 = ITEMS.ID2) AND (CATEGORIES.ID1 = ITEMS.ID1))'].
68
+ should include(MSSQL_DB[:items].join(:categories, [:id1, :id2]).sql)
68
69
  MSSQL_DB[:items___i].join(:categories___c, [:id]).sql.should ==
69
70
  'SELECT * FROM ITEMS AS I INNER JOIN CATEGORIES AS C ON (C.ID = I.ID)'
70
71
  end
@@ -401,3 +402,36 @@ context "A MSSQL database" do
401
402
  (s[:max_chars] || s[:column_size]).should == 2
402
403
  end
403
404
  end
405
+
406
+ context "MSSQL::Database#rename_table" do
407
+ specify "should work on non-schema bound tables which need escaping" do
408
+ MSSQL_DB.quote_identifiers = true
409
+ MSSQL_DB.create_table! :'foo bar' do
410
+ text :name
411
+ end
412
+ MSSQL_DB.drop_table :baz rescue nil
413
+ proc { MSSQL_DB.rename_table 'foo bar', 'baz' }.should_not raise_error
414
+ end
415
+
416
+ specify "should workd on schema bound tables" do
417
+ MSSQL_DB.execute(<<-SQL)
418
+ IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = 'MY')
419
+ EXECUTE sp_executesql N'create schema MY'
420
+ SQL
421
+ MSSQL_DB.create_table! :MY__foo do
422
+ text :name
423
+ end
424
+ proc { MSSQL_DB.rename_table :MY__foo, :MY__bar }.should_not raise_error
425
+ proc { MSSQL_DB.rename_table :MY__bar, :foo }.should_not raise_error
426
+ end
427
+ end
428
+
429
+ context "MSSQL::Dataset#count" do
430
+ specify "should work with a distinct query with an order clause" do
431
+ MSSQL_DB.create_table!(:items){String :name; Integer :value}
432
+ MSSQL_DB[:items].insert(:name => "name", :value => 1)
433
+ MSSQL_DB[:items].insert(:name => "name", :value => 1)
434
+ MSSQL_DB[:items].select(:name, :value).distinct.order(:name).count.should == 1
435
+ MSSQL_DB[:items].select(:name, :value).group(:name, :value).order(:name).count.should == 1
436
+ end
437
+ end