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.
- data/CHANGELOG +108 -0
- data/README.rdoc +25 -14
- data/Rakefile +20 -1
- data/doc/advanced_associations.rdoc +61 -64
- data/doc/cheat_sheet.rdoc +16 -7
- data/doc/opening_databases.rdoc +3 -3
- data/doc/prepared_statements.rdoc +1 -1
- data/doc/reflection.rdoc +2 -1
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/schema.rdoc +19 -14
- data/lib/sequel/adapters/amalgalite.rb +5 -27
- data/lib/sequel/adapters/jdbc.rb +13 -3
- data/lib/sequel/adapters/jdbc/h2.rb +17 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
- data/lib/sequel/adapters/mysql.rb +4 -3
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +87 -28
- data/lib/sequel/adapters/shared/mssql.rb +47 -6
- data/lib/sequel/adapters/shared/mysql.rb +12 -31
- data/lib/sequel/adapters/shared/postgres.rb +15 -12
- data/lib/sequel/adapters/shared/sqlite.rb +18 -0
- data/lib/sequel/adapters/sqlite.rb +1 -16
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +2 -0
- data/lib/sequel/database/schema_sql.rb +1 -1
- data/lib/sequel/dataset.rb +5 -179
- data/lib/sequel/dataset/actions.rb +123 -0
- data/lib/sequel/dataset/convenience.rb +18 -10
- data/lib/sequel/dataset/features.rb +65 -0
- data/lib/sequel/dataset/prepared_statements.rb +29 -23
- data/lib/sequel/dataset/query.rb +429 -0
- data/lib/sequel/dataset/sql.rb +67 -435
- data/lib/sequel/model/associations.rb +77 -13
- data/lib/sequel/model/base.rb +30 -8
- data/lib/sequel/model/errors.rb +4 -4
- data/lib/sequel/plugins/caching.rb +38 -15
- data/lib/sequel/plugins/force_encoding.rb +18 -7
- data/lib/sequel/plugins/hook_class_methods.rb +4 -0
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +40 -11
- data/lib/sequel/plugins/serialization.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +65 -18
- data/lib/sequel/sql.rb +23 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +96 -10
- data/spec/adapters/mysql_spec.rb +19 -0
- data/spec/adapters/postgres_spec.rb +65 -2
- data/spec/adapters/sqlite_spec.rb +10 -0
- data/spec/core/core_sql_spec.rb +9 -0
- data/spec/core/database_spec.rb +8 -4
- data/spec/core/dataset_spec.rb +122 -29
- data/spec/core/expression_filters_spec.rb +17 -0
- data/spec/extensions/caching_spec.rb +43 -3
- data/spec/extensions/force_encoding_spec.rb +43 -1
- data/spec/extensions/nested_attributes_spec.rb +55 -2
- data/spec/extensions/validation_helpers_spec.rb +71 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/dataset_test.rb +383 -9
- data/spec/integration/eager_loader_test.rb +0 -65
- data/spec/integration/model_test.rb +110 -0
- data/spec/integration/plugin_test.rb +306 -0
- data/spec/integration/prepared_statement_test.rb +32 -0
- data/spec/integration/schema_test.rb +8 -3
- data/spec/integration/spec_helper.rb +1 -25
- data/spec/model/association_reflection_spec.rb +38 -0
- data/spec/model/associations_spec.rb +184 -8
- data/spec/model/eager_loading_spec.rb +23 -0
- data/spec/model/model_spec.rb +8 -0
- data/spec/model/record_spec.rb +84 -1
- 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
|
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
|
-
|
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
|
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.
|
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
|
15
|
-
# If you use yaml, you should require
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/sequel/sql.rb
CHANGED
@@ -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)
|
data/lib/sequel/version.rb
CHANGED
data/spec/adapters/mssql_spec.rb
CHANGED
@@ -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 :
|
177
|
+
attr_reader :import_sqls
|
120
178
|
|
121
179
|
def execute(sql, opts={})
|
122
|
-
@
|
123
|
-
@
|
180
|
+
@import_sqls ||= []
|
181
|
+
@import_sqls << sql
|
124
182
|
end
|
125
183
|
alias execute_dui execute
|
126
184
|
|
127
185
|
def transaction(opts={})
|
128
|
-
@
|
129
|
-
@
|
186
|
+
@import_sqls ||= []
|
187
|
+
@import_sqls << 'BEGIN'
|
130
188
|
yield
|
131
|
-
@
|
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.
|
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
|