sequel 3.13.0 → 3.14.0

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