sequel 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/CHANGELOG +108 -0
  2. data/README.rdoc +25 -14
  3. data/Rakefile +20 -1
  4. data/doc/advanced_associations.rdoc +61 -64
  5. data/doc/cheat_sheet.rdoc +16 -7
  6. data/doc/opening_databases.rdoc +3 -3
  7. data/doc/prepared_statements.rdoc +1 -1
  8. data/doc/reflection.rdoc +2 -1
  9. data/doc/release_notes/3.6.0.txt +366 -0
  10. data/doc/schema.rdoc +19 -14
  11. data/lib/sequel/adapters/amalgalite.rb +5 -27
  12. data/lib/sequel/adapters/jdbc.rb +13 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
  15. data/lib/sequel/adapters/mysql.rb +4 -3
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +87 -28
  18. data/lib/sequel/adapters/shared/mssql.rb +47 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +12 -31
  20. data/lib/sequel/adapters/shared/postgres.rb +15 -12
  21. data/lib/sequel/adapters/shared/sqlite.rb +18 -0
  22. data/lib/sequel/adapters/sqlite.rb +1 -16
  23. data/lib/sequel/connection_pool.rb +1 -1
  24. data/lib/sequel/core.rb +1 -1
  25. data/lib/sequel/database.rb +1 -1
  26. data/lib/sequel/database/schema_generator.rb +2 -0
  27. data/lib/sequel/database/schema_sql.rb +1 -1
  28. data/lib/sequel/dataset.rb +5 -179
  29. data/lib/sequel/dataset/actions.rb +123 -0
  30. data/lib/sequel/dataset/convenience.rb +18 -10
  31. data/lib/sequel/dataset/features.rb +65 -0
  32. data/lib/sequel/dataset/prepared_statements.rb +29 -23
  33. data/lib/sequel/dataset/query.rb +429 -0
  34. data/lib/sequel/dataset/sql.rb +67 -435
  35. data/lib/sequel/model/associations.rb +77 -13
  36. data/lib/sequel/model/base.rb +30 -8
  37. data/lib/sequel/model/errors.rb +4 -4
  38. data/lib/sequel/plugins/caching.rb +38 -15
  39. data/lib/sequel/plugins/force_encoding.rb +18 -7
  40. data/lib/sequel/plugins/hook_class_methods.rb +4 -0
  41. data/lib/sequel/plugins/many_through_many.rb +1 -1
  42. data/lib/sequel/plugins/nested_attributes.rb +40 -11
  43. data/lib/sequel/plugins/serialization.rb +17 -3
  44. data/lib/sequel/plugins/validation_helpers.rb +65 -18
  45. data/lib/sequel/sql.rb +23 -1
  46. data/lib/sequel/version.rb +1 -1
  47. data/spec/adapters/mssql_spec.rb +96 -10
  48. data/spec/adapters/mysql_spec.rb +19 -0
  49. data/spec/adapters/postgres_spec.rb +65 -2
  50. data/spec/adapters/sqlite_spec.rb +10 -0
  51. data/spec/core/core_sql_spec.rb +9 -0
  52. data/spec/core/database_spec.rb +8 -4
  53. data/spec/core/dataset_spec.rb +122 -29
  54. data/spec/core/expression_filters_spec.rb +17 -0
  55. data/spec/extensions/caching_spec.rb +43 -3
  56. data/spec/extensions/force_encoding_spec.rb +43 -1
  57. data/spec/extensions/nested_attributes_spec.rb +55 -2
  58. data/spec/extensions/validation_helpers_spec.rb +71 -0
  59. data/spec/integration/associations_test.rb +281 -0
  60. data/spec/integration/dataset_test.rb +383 -9
  61. data/spec/integration/eager_loader_test.rb +0 -65
  62. data/spec/integration/model_test.rb +110 -0
  63. data/spec/integration/plugin_test.rb +306 -0
  64. data/spec/integration/prepared_statement_test.rb +32 -0
  65. data/spec/integration/schema_test.rb +8 -3
  66. data/spec/integration/spec_helper.rb +1 -25
  67. data/spec/model/association_reflection_spec.rb +38 -0
  68. data/spec/model/associations_spec.rb +184 -8
  69. data/spec/model/eager_loading_spec.rb +23 -0
  70. data/spec/model/model_spec.rb +8 -0
  71. data/spec/model/record_spec.rb +84 -1
  72. metadata +9 -2
@@ -34,18 +34,29 @@ module Sequel
34
34
  super
35
35
  subclass.forced_encoding = forced_encoding
36
36
  end
37
-
38
- # Force the encoding of all strings for the given row to the model's
39
- # forced_encoding.
40
- def load(row)
41
- row.values.each{|v| v.force_encoding(forced_encoding) if v.is_a?(String)} if forced_encoding
42
- super
43
- end
44
37
  end
45
38
 
46
39
  module InstanceMethods
40
+ # Allow the force encoding plugin to work with the identity_map
41
+ # plugin by typecasting new values.
42
+ def merge_db_update(row)
43
+ super(force_hash_encoding(row))
44
+ end
45
+
47
46
  private
48
47
 
48
+ # Force the encoding for all string values in the given row hash.
49
+ def force_hash_encoding(row)
50
+ fe = model.forced_encoding
51
+ row.values.each{|v| v.force_encoding(fe) if v.is_a?(String)} if fe
52
+ row
53
+ end
54
+
55
+ # Force the encoding of all string values when setting the instance's values.
56
+ def set_values(row)
57
+ super(force_hash_encoding(row))
58
+ end
59
+
49
60
  # Force the encoding of all returned strings to the model's forced_encoding.
50
61
  def typecast_value(column, value)
51
62
  s = super
@@ -19,6 +19,10 @@ module Sequel
19
19
  # return false if super == false
20
20
  # self.created_at = Time.now
21
21
  # end
22
+ #
23
+ # Note that returning false in any before hook block will skip further
24
+ # before hooks and abort the action. So if a before_save hook block returns
25
+ # false, future before_save hook blocks are not called, and the save is aborted.
22
26
  module HookClassMethods
23
27
  # Set up the hooks instance variable in the model.
24
28
  def self.apply(model)
@@ -156,7 +156,7 @@ module Sequel
156
156
  uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
157
157
  left_keys = Array(left_key)
158
158
  left_pk = (opts[:left_primary_key] ||= self.primary_key)
159
- left_pks = Array(left_pk)
159
+ left_pks = opts[:left_primary_keys] = Array(left_pk)
160
160
  opts[:dataset] ||= lambda do
161
161
  ds = opts.associated_class
162
162
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
@@ -27,6 +27,8 @@ module Sequel
27
27
 
28
28
  # Allow nested attributes to be set for the given associations. Options:
29
29
  # * :destroy - Allow destruction of nested records.
30
+ # * :fields - If provided, should be an Array. Restricts the fields allowed to be
31
+ # modified through the association_attributes= method to the specific fields given.
30
32
  # * :limit - For *_to_many associations, a limit on the number of records
31
33
  # that will be processed, to prevent denial of service attacks.
32
34
  # * :remove - Allow disassociation of nested records (can remove the associated
@@ -68,26 +70,32 @@ module Sequel
68
70
  end
69
71
 
70
72
  module InstanceMethods
71
- # Assume if an instance has nested attributes set on it, that it has
72
- # been modified even if none of the instance's columns have been modified.
73
- def modified?
74
- super || @has_nested_attributes
75
- end
76
-
77
73
  private
78
74
 
75
+ # Check that the keys related to the association are not modified inside the block. Does
76
+ # not use an ensure block, so callers should be careful.
77
+ def nested_attributes_check_key_modifications(reflection, obj)
78
+ keys = reflection.associated_object_keys.map{|x| obj.send(x)}
79
+ yield
80
+ raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed") unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
81
+ end
82
+
79
83
  # Create a new associated object with the given attributes, validate
80
84
  # it when the parent is validated, and save it when the object is saved.
85
+ # Returns the object created.
81
86
  def nested_attributes_create(reflection, attributes)
82
- obj = reflection.associated_class.new(attributes)
87
+ obj = reflection.associated_class.new
88
+ nested_attributes_set_attributes(reflection, obj, attributes)
83
89
  after_validation_hook{validate_associated_object(reflection, obj)}
84
90
  if reflection.returns_array?
91
+ send(reflection[:name]) << obj
85
92
  after_save_hook{send(reflection.add_method, obj)}
86
93
  else
87
94
  # Don't need to validate the object twice if :validate association option is not false
88
95
  # and don't want to validate it at all if it is false.
89
96
  before_save_hook{send(reflection.setter_method, obj.save(:validate=>false))}
90
97
  end
98
+ obj
91
99
  end
92
100
 
93
101
  # Find an associated object with the matching pk. If a matching option
@@ -114,6 +122,7 @@ module Sequel
114
122
 
115
123
  # Remove the matching associated object from the current object.
116
124
  # If the :destroy option is given, destroy the object after disassociating it.
125
+ # Returns the object removed, if it exists.
117
126
  def nested_attributes_remove(reflection, pk, opts={})
118
127
  if obj = nested_attributes_find(reflection, pk)
119
128
  before_save_hook do
@@ -124,9 +133,20 @@ module Sequel
124
133
  end
125
134
  end
126
135
  after_save_hook{obj.destroy} if opts[:destroy]
136
+ obj
127
137
  end
128
138
  end
129
139
 
140
+ # Set the fields in the obj based on the association, only allowing
141
+ # specific :fields if configured.
142
+ def nested_attributes_set_attributes(reflection, obj, attributes)
143
+ if fields = reflection[:nested_attributes][:fields]
144
+ obj.set_only(attributes, fields)
145
+ else
146
+ obj.set(attributes)
147
+ end
148
+ end
149
+
130
150
  # Modify the associated object based on the contents of the attribtues hash:
131
151
  # * If a block was given to nested_attributes, call it with the attributes and return immediately if the block returns true.
132
152
  # * If no primary key exists in the attributes hash, create a new object.
@@ -135,7 +155,7 @@ module Sequel
135
155
  # * Otherwise, update the matching associated object with the contents of the hash.
136
156
  def nested_attributes_setter(reflection, attributes)
137
157
  return if (b = reflection[:nested_attributes][:reject_if]) && b.call(attributes)
138
- @has_nested_attributes = true
158
+ modified!
139
159
  klass = reflection.associated_class
140
160
  if pk = attributes.delete(klass.primary_key) || attributes.delete(klass.primary_key.to_s)
141
161
  if klass.db.send(:typecast_value_boolean, attributes[:_delete] || attributes['_delete']) && reflection[:nested_attributes][:destroy]
@@ -153,16 +173,25 @@ module Sequel
153
173
  # Update the matching associated object with the attributes,
154
174
  # validating it when the parent object is validated and saving it
155
175
  # when the parent is saved.
176
+ # Returns the object updated, if it exists.
156
177
  def nested_attributes_update(reflection, pk, attributes)
157
178
  if obj = nested_attributes_find(reflection, pk)
158
- obj.set(attributes)
179
+ nested_attributes_update_attributes(reflection, obj, attributes)
159
180
  after_validation_hook{validate_associated_object(reflection, obj)}
160
181
  # Don't need to validate the object twice if :validate association option is not false
161
182
  # and don't want to validate it at all if it is false.
162
- after_save_hook{obj.save(:validate=>false)}
183
+ after_save_hook{obj.save_changes(:validate=>false)}
184
+ obj
163
185
  end
164
186
  end
165
-
187
+
188
+ # Update the attributes for the given object related to the current object through the association.
189
+ def nested_attributes_update_attributes(reflection, obj, attributes)
190
+ nested_attributes_check_key_modifications(reflection, obj) do
191
+ nested_attributes_set_attributes(reflection, obj, attributes)
192
+ end
193
+ end
194
+
166
195
  # Validate the given associated object, adding any validation error messages from the
167
196
  # given object to the parent object.
168
197
  def validate_associated_object(reflection, obj)
@@ -3,7 +3,7 @@ module Sequel
3
3
  # Sequel's built in Serialization plugin allows you to keep serialized
4
4
  # ruby objects in the database, while giving you deserialized objects
5
5
  # when you call an accessor.
6
- #
6
+ #
7
7
  # This plugin works by keeping the serialized value in the values, and
8
8
  # adding a @deserialized_values hash. The reader method for serialized columns
9
9
  # will check the @deserialized_values for the value, return it if present,
@@ -11,13 +11,27 @@ module Sequel
11
11
  # set the @deserialized_values entry. This plugin adds a before_save hook
12
12
  # that serializes all @deserialized_values to @values.
13
13
  #
14
- # You can use either marshal or yaml as the serialization format.
15
- # If you use yaml, you should require yaml yourself.
14
+ # You can use either marshal, yaml, or json as the serialization format.
15
+ # If you use yaml or json, you should require them by yourself.
16
16
  #
17
17
  # Because of how this plugin works, it must be used inside each model class
18
18
  # that needs serialization, after any set_dataset method calls in that class.
19
19
  # Otherwise, it is possible that the default column accessors will take
20
20
  # precedence.
21
+ #
22
+ # == Example
23
+ #
24
+ # require 'sequel'
25
+ # require 'json'
26
+ # class User < Sequel::Model
27
+ # plugin :serialization, :json, :permissions
28
+ # # or
29
+ # plugin :serialization
30
+ # serialize_attributes :marshal, :permissions, :attributes
31
+ # end
32
+ # user = User.create
33
+ # user.permissions = { :global => 'read-only' }
34
+ # user.save
21
35
  module Serialization
22
36
  # Set up the column readers to do deserialization and the column writers
23
37
  # to save the value in deserialized_values.
@@ -30,49 +30,75 @@ module Sequel
30
30
  # Sequel will attempt to insert a NULL value into the database, instead of using the
31
31
  # database's default.
32
32
  # * :allow_nil - Whether to skip the validation if the value is nil.
33
- # * :message - The message to use
33
+ # * :message - The message to use. Can be a string which is used directly, or a
34
+ # proc which is called. If the validation method takes a argument before the array of attributes,
35
+ # that argument is passed as an argument to the proc. The exception is the
36
+ # validates_not_string method, which doesn't take an argument, but passes
37
+ # the schema type symbol as the argument to the proc.
38
+ #
39
+ # The default validation options for all models can be modified by
40
+ # changing the values of the Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS hash. You
41
+ # change change the default options on a per model basis
42
+ # by overriding a private instance method default_validation_helpers_options.
34
43
  module ValidationHelpers
44
+ # Default validation options used by Sequel. Can be modified to change the error
45
+ # messages for all models (e.g. for internationalization), or to set certain
46
+ # default options for validations (e.g. :allow_nil=>true for all validates_format).
47
+ DEFAULT_OPTIONS = {
48
+ :exact_length=>{:message=>lambda{|exact| "is not #{exact} characters"}},
49
+ :format=>{:message=>lambda{|with| 'is invalid'}},
50
+ :includes=>{:message=>lambda{|set| "is not in range or set: #{set.inspect}"}},
51
+ :integer=>{:message=>lambda{"is not a number"}},
52
+ :length_range=>{:message=>lambda{|range| "is too short or too long"}},
53
+ :max_length=>{:message=>lambda{|max| "is longer than #{max} characters"}},
54
+ :min_length=>{:message=>lambda{|min| "is shorter than #{min} characters"}},
55
+ :not_string=>{:message=>lambda{|type| type ? "is not a valid #{type}" : "is a string"}},
56
+ :numeric=>{:message=>lambda{"is not a number"}},
57
+ :presence=>{:message=>lambda{"is not present"}},
58
+ :unique=>{:message=>lambda{'is already taken'}}
59
+ }
60
+
35
61
  module InstanceMethods
36
62
  # Check that the attribute values are the given exact length.
37
63
  def validates_exact_length(exact, atts, opts={})
38
- validatable_attributes(atts, opts){|a,v,m| (m || "is not #{exact} characters") unless v && v.length == exact}
64
+ validatable_attributes_for_type(:exact_length, atts, opts){|a,v,m| validation_error_message(m, exact) unless v && v.length == exact}
39
65
  end
40
66
 
41
67
  # Check the string representation of the attribute value(s) against the regular expression with.
42
68
  def validates_format(with, atts, opts={})
43
- validatable_attributes(atts, opts){|a,v,m| (m || 'is invalid') unless v.to_s =~ with}
69
+ validatable_attributes_for_type(:format, atts, opts){|a,v,m| validation_error_message(m, with) unless v.to_s =~ with}
44
70
  end
45
71
 
46
72
  # Check attribute value(s) is included in the given set.
47
73
  def validates_includes(set, atts, opts={})
48
- validatable_attributes(atts, opts){|a,v,m| (m || "is not in range or set: #{set.inspect}") unless set.include?(v)}
74
+ validatable_attributes_for_type(:includes, atts, opts){|a,v,m| validation_error_message(m, set) unless set.include?(v)}
49
75
  end
50
76
 
51
77
  # Check attribute value(s) string representation is a valid integer.
52
78
  def validates_integer(atts, opts={})
53
- validatable_attributes(atts, opts) do |a,v,m|
79
+ validatable_attributes_for_type(:integer, atts, opts) do |a,v,m|
54
80
  begin
55
81
  Kernel.Integer(v.to_s)
56
82
  nil
57
83
  rescue
58
- m || 'is not a number'
84
+ validation_error_message(m)
59
85
  end
60
86
  end
61
87
  end
62
88
 
63
89
  # Check that the attribute values length is in the specified range.
64
90
  def validates_length_range(range, atts, opts={})
65
- validatable_attributes(atts, opts){|a,v,m| (m || "is outside the allowed range") unless v && range.include?(v.length)}
91
+ validatable_attributes_for_type(:length_range, atts, opts){|a,v,m| validation_error_message(m, range) unless v && range.include?(v.length)}
66
92
  end
67
93
 
68
94
  # Check that the attribute values are not longer than the given max length.
69
95
  def validates_max_length(max, atts, opts={})
70
- validatable_attributes(atts, opts){|a,v,m| (m || "is longer than #{max} characters") unless v && v.length <= max}
96
+ validatable_attributes_for_type(:max_length, atts, opts){|a,v,m| validation_error_message(m, max) unless v && v.length <= max}
71
97
  end
72
98
 
73
99
  # Check that the attribute values are not shorter than the given min length.
74
100
  def validates_min_length(min, atts, opts={})
75
- validatable_attributes(atts, opts){|a,v,m| (m || "is shorter than #{min} characters") unless v && v.length >= min}
101
+ validatable_attributes_for_type(:min_length, atts, opts){|a,v,m| validation_error_message(m, min) unless v && v.length >= min}
76
102
  end
77
103
 
78
104
  # Check that the attribute value(s) is not a string. This is generally useful
@@ -82,28 +108,24 @@ module Sequel
82
108
  # be a string in an invalid format, and if typecasting succeeds, the value will
83
109
  # not be a string.
84
110
  def validates_not_string(atts, opts={})
85
- validatable_attributes(atts, opts) do |a,v,m|
86
- next unless v.is_a?(String)
87
- next m if m
88
- (sch = db_schema[a] and typ = sch[:type]) ? "is not a valid #{typ}" : "is a string"
89
- end
111
+ validatable_attributes_for_type(:not_string, atts, opts){|a,v,m| validation_error_message(m, (db_schema[a]||{})[:type]) if v.is_a?(String)}
90
112
  end
91
113
 
92
114
  # Check attribute value(s) string representation is a valid float.
93
115
  def validates_numeric(atts, opts={})
94
- validatable_attributes(atts, opts) do |a,v,m|
116
+ validatable_attributes_for_type(:numeric, atts, opts) do |a,v,m|
95
117
  begin
96
118
  Kernel.Float(v.to_s)
97
119
  nil
98
120
  rescue
99
- m || 'is not a number'
121
+ validation_error_message(m)
100
122
  end
101
123
  end
102
124
  end
103
125
 
104
126
  # Check attribute value(s) is not considered blank by the database, but allow false values.
105
127
  def validates_presence(atts, opts={})
106
- validatable_attributes(atts, opts){|a,v,m| (m || "is not present") if model.db.send(:blank_object?, v) && v != false}
128
+ validatable_attributes_for_type(:presence, atts, opts){|a,v,m| validation_error_message(m) if model.db.send(:blank_object?, v) && v != false}
107
129
  end
108
130
 
109
131
  # Checks that there are no duplicate values in the database for the given
@@ -132,7 +154,11 @@ module Sequel
132
154
  # Possible Options:
133
155
  # * :message - The message to use (default: 'is already taken')
134
156
  def validates_unique(*atts)
135
- message = (atts.pop[:message] if atts.last.is_a?(Hash)) || 'is already taken'
157
+ opts = default_validation_helpers_options(:unique)
158
+ if atts.last.is_a?(Hash)
159
+ opts = opts.merge(atts.pop)
160
+ end
161
+ message = validation_error_message(opts[:message])
136
162
  atts.each do |a|
137
163
  ds = model.filter(Array(a).map{|x| [x, send(x)]})
138
164
  ds = yield(ds) if block_given?
@@ -141,6 +167,14 @@ module Sequel
141
167
  end
142
168
 
143
169
  private
170
+
171
+ # The default options hash for the given type of validation. Can
172
+ # be overridden on a per-model basis for different per model defaults.
173
+ # The hash return must include a :message option that is either a
174
+ # proc or string.
175
+ def default_validation_helpers_options(type)
176
+ DEFAULT_OPTIONS[type]
177
+ end
144
178
 
145
179
  # Skip validating any attribute that matches one of the allow_* options.
146
180
  # Otherwise, yield the attribute, value, and passed option :message to
@@ -158,6 +192,19 @@ module Sequel
158
192
  end
159
193
  end
160
194
  end
195
+
196
+ # Merge the given options with the default options for the given type
197
+ # and call validatable_attributes with the merged options.
198
+ def validatable_attributes_for_type(type, atts, opts, &block)
199
+ validatable_attributes(atts, default_validation_helpers_options(type).merge(opts), &block)
200
+ end
201
+
202
+ # The validation error message to use, as a string. If message
203
+ # is a Proc, call it with the args. Otherwise, assume it is a string and
204
+ # return it.
205
+ def validation_error_message(message, *args)
206
+ message.is_a?(Proc) ? message.call(*args) : message
207
+ end
161
208
  end
162
209
  end
163
210
  end
@@ -6,6 +6,19 @@ module Sequel
6
6
  class BasicObject
7
7
  (instance_methods - %w"__id__ __send__ instance_eval == equal?").each{|m| undef_method(m)}
8
8
  end
9
+ else
10
+ # If on 1.9, create a Sequel::BasicObject class that is just like the
11
+ # default BasicObject class, except that missing constants are resolved in
12
+ # Object. This allows the virtual row support to work with classes
13
+ # without prefixing them with ::, such as:
14
+ #
15
+ # DB[:bonds].filter{maturity_date > Time.now}
16
+ class BasicObject < ::BasicObject
17
+ # Lookup missing constants in ::Object
18
+ def self.const_missing(name)
19
+ ::Object.const_get(name)
20
+ end
21
+ end
9
22
  end
10
23
 
11
24
  class LiteralString < ::String
@@ -36,6 +49,11 @@ module Sequel
36
49
  def lit
37
50
  self
38
51
  end
52
+
53
+ # Alias for to_s
54
+ def sql_literal(ds)
55
+ to_s(ds)
56
+ end
39
57
  end
40
58
 
41
59
  # Represents a complex SQL expression, with a given operator and one
@@ -652,6 +670,9 @@ module Sequel
652
670
  # required for the prepared statement support
653
671
  class PlaceholderLiteralString < Expression
654
672
  # The arguments that will be subsituted into the placeholders.
673
+ # Either an array of unnamed placeholders (which will be substituted in
674
+ # order for ? characters), or a hash of named placeholders (which will be
675
+ # substituted for :key phrases).
655
676
  attr_reader :args
656
677
 
657
678
  # The literal string containing placeholders
@@ -663,7 +684,7 @@ module Sequel
663
684
  # Create an object with the given string, placeholder arguments, and parens flag.
664
685
  def initialize(str, args, parens=false)
665
686
  @str = str
666
- @args = args
687
+ @args = args.is_a?(Array) && args.length == 1 && (v = args.at(0)).is_a?(Hash) ? v : args
667
688
  @parens = parens
668
689
  end
669
690
 
@@ -783,6 +804,7 @@ module Sequel
783
804
  class SQLArray < Expression
784
805
  # The array of objects this SQLArray wraps
785
806
  attr_reader :array
807
+ alias to_a array
786
808
 
787
809
  # Create an object with the given array.
788
810
  def initialize(array)
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  MAJOR = 3
3
- MINOR = 5
3
+ MINOR = 6
4
4
  TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join('.')
@@ -6,6 +6,32 @@ unless defined?(MSSQL_DB)
6
6
  end
7
7
  INTEGRATION_DB = MSSQL_DB unless defined?(INTEGRATION_DB)
8
8
 
9
+ def MSSQL_DB.sqls
10
+ (@sqls ||= [])
11
+ end
12
+ logger = Object.new
13
+ def logger.method_missing(m, msg)
14
+ MSSQL_DB.sqls << msg
15
+ end
16
+ MSSQL_DB.logger = logger
17
+
18
+ MSSQL_DB.create_table! :test do
19
+ text :name
20
+ integer :value, :index => true
21
+ end
22
+ MSSQL_DB.create_table! :test2 do
23
+ text :name
24
+ integer :value
25
+ end
26
+ MSSQL_DB.create_table! :test3 do
27
+ integer :value
28
+ timestamp :time
29
+ end
30
+ MSSQL_DB.create_table! :test4 do
31
+ varchar :name, :size => 20
32
+ varbinary :value
33
+ end
34
+
9
35
  context "A MSSQL database" do
10
36
  before do
11
37
  @db = MSSQL_DB
@@ -27,6 +53,17 @@ context "A MSSQL database" do
27
53
  end
28
54
  end
29
55
 
56
+ context "MSSQL Dataset#join_table" do
57
+ specify "should emulate the USING clause with ON" do
58
+ MSSQL_DB[:items].join(:categories, [:id]).sql.should ==
59
+ 'SELECT * FROM ITEMS INNER JOIN CATEGORIES ON (CATEGORIES.ID = ITEMS.ID)'
60
+ MSSQL_DB[:items].join(:categories, [:id1, :id2]).sql.should ==
61
+ 'SELECT * FROM ITEMS INNER JOIN CATEGORIES ON ((CATEGORIES.ID1 = ITEMS.ID1) AND (CATEGORIES.ID2 = ITEMS.ID2))'
62
+ MSSQL_DB[:items___i].join(:categories___c, [:id]).sql.should ==
63
+ 'SELECT * FROM ITEMS AS I INNER JOIN CATEGORIES AS C ON (C.ID = I.ID)'
64
+ end
65
+ end
66
+
30
67
  context "MSSQL Dataset#output" do
31
68
  before do
32
69
  @db = MSSQL_DB
@@ -39,21 +76,42 @@ context "MSSQL Dataset#output" do
39
76
  @db.drop_table(:out)
40
77
  end
41
78
 
42
- specify "should format OUTPUT clauses for DELETE statements" do
79
+ specify "should format OUTPUT clauses without INTO for DELETE statements" do
80
+ @ds.output(nil, [:deleted__name, :deleted__value]).delete_sql.should =~
81
+ /DELETE FROM ITEMS OUTPUT DELETED.(NAME|VALUE), DELETED.(NAME|VALUE)/
82
+ @ds.output(nil, [:deleted.*]).delete_sql.should =~
83
+ /DELETE FROM ITEMS OUTPUT DELETED.*/
84
+ end
85
+
86
+ specify "should format OUTPUT clauses with INTO for DELETE statements" do
43
87
  @ds.output(:out, [:deleted__name, :deleted__value]).delete_sql.should =~
44
88
  /DELETE FROM ITEMS OUTPUT DELETED.(NAME|VALUE), DELETED.(NAME|VALUE) INTO OUT/
45
89
  @ds.output(:out, {:name => :deleted__name, :value => :deleted__value}).delete_sql.should =~
46
90
  /DELETE FROM ITEMS OUTPUT DELETED.(NAME|VALUE), DELETED.(NAME|VALUE) INTO OUT \((NAME|VALUE), (NAME|VALUE)\)/
47
91
  end
48
92
 
49
- specify "should format OUTPUT clauses for INSERT statements" do
93
+ specify "should format OUTPUT clauses without INTO for INSERT statements" do
94
+ @ds.output(nil, [:inserted__name, :inserted__value]).insert_sql(:name => "name", :value => 1).should =~
95
+ /INSERT INTO ITEMS \((NAME|VALUE), (NAME|VALUE)\) OUTPUT INSERTED.(NAME|VALUE), INSERTED.(NAME|VALUE) VALUES \((N'name'|1), (N'name'|1)\)/
96
+ @ds.output(nil, [:inserted.*]).insert_sql(:name => "name", :value => 1).should =~
97
+ /INSERT INTO ITEMS \((NAME|VALUE), (NAME|VALUE)\) OUTPUT INSERTED.* VALUES \((N'name'|1), (N'name'|1)\)/
98
+ end
99
+
100
+ specify "should format OUTPUT clauses with INTO for INSERT statements" do
50
101
  @ds.output(:out, [:inserted__name, :inserted__value]).insert_sql(:name => "name", :value => 1).should =~
51
102
  /INSERT INTO ITEMS \((NAME|VALUE), (NAME|VALUE)\) OUTPUT INSERTED.(NAME|VALUE), INSERTED.(NAME|VALUE) INTO OUT VALUES \((N'name'|1), (N'name'|1)\)/
52
103
  @ds.output(:out, {:name => :inserted__name, :value => :inserted__value}).insert_sql(:name => "name", :value => 1).should =~
53
104
  /INSERT INTO ITEMS \((NAME|VALUE), (NAME|VALUE)\) OUTPUT INSERTED.(NAME|VALUE), INSERTED.(NAME|VALUE) INTO OUT \((NAME|VALUE), (NAME|VALUE)\) VALUES \((N'name'|1), (N'name'|1)\)/
54
105
  end
55
106
 
56
- specify "should format OUTPUT clauses for UPDATE statements" do
107
+ specify "should format OUTPUT clauses without INTO for UPDATE statements" do
108
+ @ds.output(nil, [:inserted__name, :deleted__value]).update_sql(:value => 2).should =~
109
+ /UPDATE ITEMS SET VALUE = 2 OUTPUT (INSERTED.NAME|DELETED.VALUE), (INSERTED.NAME|DELETED.VALUE)/
110
+ @ds.output(nil, [:inserted.*]).update_sql(:value => 2).should =~
111
+ /UPDATE ITEMS SET VALUE = 2 OUTPUT INSERTED.*/
112
+ end
113
+
114
+ specify "should format OUTPUT clauses with INTO for UPDATE statements" do
57
115
  @ds.output(:out, [:inserted__name, :deleted__value]).update_sql(:value => 2).should =~
58
116
  /UPDATE ITEMS SET VALUE = 2 OUTPUT (INSERTED.NAME|DELETED.VALUE), (INSERTED.NAME|DELETED.VALUE) INTO OUT/
59
117
  @ds.output(:out, {:name => :inserted__name, :value => :deleted__value}).update_sql(:value => 2).should =~
@@ -116,26 +174,26 @@ context "MSSQL dataset" do
116
174
  before do
117
175
  @db = @db.clone
118
176
  class << @db
119
- attr_reader :sqls
177
+ attr_reader :import_sqls
120
178
 
121
179
  def execute(sql, opts={})
122
- @sqls ||= []
123
- @sqls << sql
180
+ @import_sqls ||= []
181
+ @import_sqls << sql
124
182
  end
125
183
  alias execute_dui execute
126
184
 
127
185
  def transaction(opts={})
128
- @sqls ||= []
129
- @sqls << 'BEGIN'
186
+ @import_sqls ||= []
187
+ @import_sqls << 'BEGIN'
130
188
  yield
131
- @sqls << 'COMMIT'
189
+ @import_sqls << 'COMMIT'
132
190
  end
133
191
  end
134
192
  end
135
193
 
136
194
  specify "should prepend INSERT statements with WITH clause" do
137
195
  @db[:items].with(:items, @db[:inventory].group(:type)).import([:x, :y], [[1, 2], [3, 4], [5, 6]], :slice => 2)
138
- @db.sqls.should == [
196
+ @db.import_sqls.should == [
139
197
  'BEGIN',
140
198
  "WITH ITEMS AS (SELECT * FROM INVENTORY GROUP BY TYPE) INSERT INTO ITEMS (X, Y) SELECT 1, 2 UNION ALL SELECT 3, 4",
141
199
  'COMMIT',
@@ -260,3 +318,31 @@ describe "Common Table Expressions" do
260
318
  end
261
319
  end
262
320
 
321
+ context "MSSSQL::Dataset#insert" do
322
+ before do
323
+ @db = MSSQL_DB
324
+ @db.create_table!(:test5){primary_key :xid; Integer :value}
325
+ @db.sqls.clear
326
+ @ds = @db[:test5]
327
+ end
328
+ after do
329
+ @db.drop_table(:test5) rescue nil
330
+ end
331
+
332
+ specify "should have insert_select return nil if disable_insert_output is used" do
333
+ @ds.disable_insert_output.insert_select(:value=>10).should == nil
334
+ end
335
+
336
+ specify "should have insert_select insert the record and return the inserted record" do
337
+ @ds.meta_def(:server_version){80201}
338
+ h = @ds.insert_select(:value=>10)
339
+ h[:value].should == 10
340
+ @ds.first(:xid=>h[:xid])[:value].should == 10
341
+ end
342
+ end
343
+
344
+ context "MSSSQL::Dataset#disable_insert_output" do
345
+ specify "should play nicely with simple_select_all?" do
346
+ MSSQL_DB[:test].disable_insert_output.send(:simple_select_all?).should == true
347
+ end
348
+ end