sequel 3.5.0 → 3.6.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 (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