sequel_core 2.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +14 -0
- data/Rakefile +1 -1
- data/lib/sequel_core/adapters/mysql.rb +5 -7
- data/lib/sequel_core/adapters/postgres.rb +20 -16
- data/lib/sequel_core/adapters/sqlite.rb +12 -0
- data/lib/sequel_core/core_ext.rb +10 -0
- data/lib/sequel_core/core_sql.rb +22 -22
- data/lib/sequel_core/database.rb +14 -8
- data/lib/sequel_core/dataset/sql.rb +25 -19
- data/lib/sequel_core/schema/sql.rb +4 -2
- data/lib/sequel_core/sql.rb +262 -155
- data/lib/sequel_core.rb +8 -0
- data/spec/adapters/mysql_spec.rb +8 -7
- data/spec/adapters/sqlite_spec.rb +23 -15
- data/spec/blockless_filters_spec.rb +44 -16
- data/spec/core_sql_spec.rb +24 -1
- data/spec/dataset_spec.rb +2 -2
- data/spec/spec_helper.rb +4 -0
- metadata +2 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
=== 2.0.1 (2008-06-04)
|
2
|
+
|
3
|
+
* Have PostgreSQL money type use BigDecimal instead of Float (jeremyevans)
|
4
|
+
|
5
|
+
* Have the PostgreSQL and MySQL adapters use the Sequel.time_class setting for datetime/timestamp types (jeremyevans)
|
6
|
+
|
7
|
+
* Add Sequel.time_class and String#to_sequel_time, used for converting time values from the database to either Time (default) or DateTime (jeremyevans)
|
8
|
+
|
9
|
+
* Make identifier quoting uppercase by default, to work better with the SQL standard, override in PostgreSQL (jeremyevans) (#232)
|
10
|
+
|
11
|
+
* Add StringExpression#+, for simple SQL string concatenation (:x.sql_string + :y) (jeremyevans)
|
12
|
+
|
13
|
+
* Refactor ComplexExpression into three subclasses and a few modules, so operators that don't make sense are not defined for the class (jeremyevans)
|
14
|
+
|
1
15
|
=== 2.0.0 (2008-06-01)
|
2
16
|
|
3
17
|
* Refactor String inflection support, you must use String.inflections instead of Inflector.inflections now (jeremyevans)
|
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "sequel_core"
|
12
|
-
VERS = "2.0.
|
12
|
+
VERS = "2.0.1"
|
13
13
|
CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
|
14
14
|
RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
|
15
15
|
'Sequel: The Database Toolkit for Ruby: Core Library and Adapters', \
|
@@ -10,12 +10,12 @@ class Mysql::Result
|
|
10
10
|
4 => :to_f, # MYSQL_TYPE_FLOAT
|
11
11
|
5 => :to_f, # MYSQL_TYPE_DOUBLE
|
12
12
|
# 6 => ??, # MYSQL_TYPE_NULL
|
13
|
-
7 => :
|
13
|
+
7 => :to_sequel_time, # MYSQL_TYPE_TIMESTAMP
|
14
14
|
8 => :to_i, # MYSQL_TYPE_LONGLONG
|
15
15
|
9 => :to_i, # MYSQL_TYPE_INT24
|
16
16
|
10 => :to_date, # MYSQL_TYPE_DATE
|
17
17
|
11 => :to_time, # MYSQL_TYPE_TIME
|
18
|
-
12 => :
|
18
|
+
12 => :to_sequel_time, # MYSQL_TYPE_DATETIME
|
19
19
|
13 => :to_i, # MYSQL_TYPE_YEAR
|
20
20
|
14 => :to_date, # MYSQL_TYPE_NEWDATE
|
21
21
|
# 15 => :to_s # MYSQL_TYPE_VARCHAR
|
@@ -268,7 +268,7 @@ module Sequel
|
|
268
268
|
def schema_ds_filter(table_name, opts)
|
269
269
|
filt = super
|
270
270
|
# Restrict it to the given or current database, unless specifically requesting :database = nil
|
271
|
-
filt = SQL::
|
271
|
+
filt = SQL::BooleanExpression.new(:AND, filt, {:c__table_schema=>opts[:database] || self.opts[:database]}) if opts[:database] || !opts.include?(:database)
|
272
272
|
filt
|
273
273
|
end
|
274
274
|
|
@@ -360,10 +360,8 @@ module Sequel
|
|
360
360
|
|
361
361
|
def complex_expression_sql(op, args)
|
362
362
|
case op
|
363
|
-
when :~, :'!~'
|
364
|
-
"#{'NOT ' if
|
365
|
-
when :'~*', :'!~*'
|
366
|
-
"#{'NOT ' if op == :'!~*'}(#{literal(args.at(0))} REGEXP #{literal(args.at(1))})"
|
363
|
+
when :~, :'!~', :'~*', :'!~*', :LIKE, :'NOT LIKE', :ILIKE, :'NOT ILIKE'
|
364
|
+
"(#{literal(args.at(0))} #{'NOT ' if [:'NOT LIKE', :'NOT ILIKE', :'!~', :'!~*'].include?(op)}#{[:~, :'!~', :'~*', :'!~*'].include?(op) ? 'REGEXP' : 'LIKE'} #{'BINARY ' if [:~, :'!~', :LIKE, :'NOT LIKE'].include?(op)}#{literal(args.at(1))})"
|
367
365
|
when :'||'
|
368
366
|
if args.length > 1
|
369
367
|
"CONCAT(#{args.collect{|a| literal(a)}.join(', ')})"
|
@@ -159,21 +159,22 @@ module Sequel
|
|
159
159
|
end
|
160
160
|
|
161
161
|
PG_TYPES = {
|
162
|
-
16 => lambda{ |s| Adapter.string_to_bool(s) },
|
163
|
-
17 => lambda{ |s| Adapter.unescape_bytea(s) },
|
164
|
-
20 => lambda{ |s| s.to_i },
|
165
|
-
21 => lambda{ |s| s.to_i },
|
166
|
-
22 => lambda{ |s| s.to_i },
|
167
|
-
23 => lambda{ |s| s.to_i },
|
168
|
-
26 => lambda{ |s| s.to_i },
|
169
|
-
700 => lambda{ |s| s.to_f },
|
170
|
-
701 => lambda{ |s| s.to_f },
|
171
|
-
790 => lambda{ |s| s.
|
172
|
-
1082 => lambda{ |s| s.to_date },
|
173
|
-
1083 => lambda{ |s| s.to_time },
|
174
|
-
1114 => lambda{ |s| s.
|
175
|
-
1184 => lambda{ |s| s.
|
176
|
-
1186 => lambda{ |s| s.to_i }
|
162
|
+
16 => lambda{ |s| Adapter.string_to_bool(s) }, # boolean
|
163
|
+
17 => lambda{ |s| Adapter.unescape_bytea(s) }, # bytea
|
164
|
+
20 => lambda{ |s| s.to_i }, # int8
|
165
|
+
21 => lambda{ |s| s.to_i }, # int2
|
166
|
+
22 => lambda{ |s| s.to_i }, # int2vector
|
167
|
+
23 => lambda{ |s| s.to_i }, # int4
|
168
|
+
26 => lambda{ |s| s.to_i }, # oid
|
169
|
+
700 => lambda{ |s| s.to_f }, # float4
|
170
|
+
701 => lambda{ |s| s.to_f }, # float8
|
171
|
+
790 => lambda{ |s| s.to_d }, # money
|
172
|
+
1082 => lambda{ |s| s.to_date }, # date
|
173
|
+
1083 => lambda{ |s| s.to_time }, # time without time zone
|
174
|
+
1114 => lambda{ |s| s.to_sequel_time }, # timestamp without time zone
|
175
|
+
1184 => lambda{ |s| s.to_sequel_time }, # timestamp with time zone
|
176
|
+
1186 => lambda{ |s| s.to_i }, # interval
|
177
|
+
1266 => lambda{ |s| s.to_time } # time with time zone
|
177
178
|
}
|
178
179
|
|
179
180
|
if Adapter.respond_to?(:translate_results=)
|
@@ -365,12 +366,15 @@ module Sequel
|
|
365
366
|
def schema_ds_filter(table_name, opts)
|
366
367
|
filt = super
|
367
368
|
# Restrict it to the given or public schema, unless specifically requesting :schema = nil
|
368
|
-
filt = SQL::
|
369
|
+
filt = SQL::BooleanExpression.new(:AND, filt, {:c__table_schema=>opts[:schema] || 'public'}) if opts[:schema] || !opts.include?(:schema)
|
369
370
|
filt
|
370
371
|
end
|
371
372
|
end
|
372
373
|
|
373
374
|
class Dataset < Sequel::Dataset
|
375
|
+
def quoted_identifier(c)
|
376
|
+
"\"#{c}\""
|
377
|
+
end
|
374
378
|
|
375
379
|
PG_TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S".freeze
|
376
380
|
|
@@ -193,6 +193,18 @@ module Sequel
|
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
196
|
+
def complex_expression_sql(op, args)
|
197
|
+
case op
|
198
|
+
when :~, :'!~', :'~*', :'!~*'
|
199
|
+
raise Error, "SQLite does not support pattern matching via regular expressions"
|
200
|
+
when :LIKE, :'NOT LIKE', :ILIKE, :'NOT ILIKE'
|
201
|
+
# SQLite is case insensitive for ASCII, and non case sensitive for other character sets
|
202
|
+
"#{'NOT ' if [:'NOT LIKE', :'NOT ILIKE'].include?(op)}(#{literal(args.at(0))} LIKE #{literal(args.at(1))})"
|
203
|
+
else
|
204
|
+
super(op, args)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
196
208
|
def insert_sql(*values)
|
197
209
|
if (values.size == 1) && values.first.is_a?(Sequel::Dataset)
|
198
210
|
"INSERT INTO #{source_list(@opts[:from])} #{values.first.sql};"
|
data/lib/sequel_core/core_ext.rb
CHANGED
@@ -183,6 +183,16 @@ class String
|
|
183
183
|
end
|
184
184
|
end
|
185
185
|
|
186
|
+
# Converts a string into a Time or DateTime object, depending on the
|
187
|
+
# value of Sequel.time_class
|
188
|
+
def to_sequel_time
|
189
|
+
begin
|
190
|
+
Sequel.time_class.parse(self)
|
191
|
+
rescue => e
|
192
|
+
raise Sequel::Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
186
196
|
# Converts a string into a Time object.
|
187
197
|
def to_time
|
188
198
|
begin
|
data/lib/sequel_core/core_sql.rb
CHANGED
@@ -2,31 +2,31 @@
|
|
2
2
|
# code.
|
3
3
|
|
4
4
|
class Array
|
5
|
-
# Return a Sequel::SQL::
|
5
|
+
# Return a Sequel::SQL::BooleanExpression created from this array, not matching any of the
|
6
6
|
# conditions.
|
7
7
|
def ~
|
8
8
|
sql_expr_if_all_two_pairs(:OR, true)
|
9
9
|
end
|
10
10
|
|
11
|
-
# Return a Sequel::SQL::
|
11
|
+
# Return a Sequel::SQL::BooleanExpression created from this array, matching all of the
|
12
12
|
# conditions.
|
13
13
|
def sql_expr
|
14
14
|
sql_expr_if_all_two_pairs
|
15
15
|
end
|
16
16
|
|
17
|
-
# Return a Sequel::SQL::
|
17
|
+
# Return a Sequel::SQL::BooleanExpression created from this array, matching none
|
18
18
|
# of the conditions.
|
19
19
|
def sql_negate
|
20
20
|
sql_expr_if_all_two_pairs(:AND, true)
|
21
21
|
end
|
22
22
|
|
23
|
-
# Return a Sequel::SQL::
|
23
|
+
# Return a Sequel::SQL::BooleanExpression created from this array, matching any of the
|
24
24
|
# conditions.
|
25
25
|
def sql_or
|
26
26
|
sql_expr_if_all_two_pairs(:OR)
|
27
27
|
end
|
28
28
|
|
29
|
-
# Return a Sequel::SQL::
|
29
|
+
# Return a Sequel::SQL::BooleanExpression representing an SQL string made up of the
|
30
30
|
# concatenation of this array's elements. If an argument is passed
|
31
31
|
# it is used in between each element of the array in the SQL
|
32
32
|
# concatenation.
|
@@ -41,7 +41,7 @@ class Array
|
|
41
41
|
args = self
|
42
42
|
end
|
43
43
|
args = args.collect{|a| a.is_one_of?(Symbol, ::Sequel::SQL::Expression, ::Sequel::LiteralString, TrueClass, FalseClass, NilClass) ? a : a.to_s}
|
44
|
-
::Sequel::SQL::
|
44
|
+
::Sequel::SQL::StringExpression.new(:'||', *args)
|
45
45
|
end
|
46
46
|
|
47
47
|
# Concatenates an array of strings into an SQL string. ANSI SQL and C-style
|
@@ -53,50 +53,50 @@ class Array
|
|
53
53
|
|
54
54
|
private
|
55
55
|
|
56
|
-
# Raise an error if this array is not made up of all two pairs, otherwise create a Sequel::SQL::
|
56
|
+
# Raise an error if this array is not made up of all two pairs, otherwise create a Sequel::SQL::BooleanExpression from this array.
|
57
57
|
def sql_expr_if_all_two_pairs(*args)
|
58
58
|
raise(Sequel::Error, 'Not all elements of the array are arrays of size 2, so it cannot be converted to an SQL expression') unless all_two_pairs?
|
59
|
-
::Sequel::SQL::
|
59
|
+
::Sequel::SQL::BooleanExpression.from_value_pairs(self, *args)
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
63
|
class Hash
|
64
|
-
# Return a Sequel::SQL::
|
64
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, matching
|
65
65
|
# all of the conditions in this hash and the condition specified by
|
66
66
|
# the given argument.
|
67
67
|
def &(ce)
|
68
|
-
::Sequel::SQL::
|
68
|
+
::Sequel::SQL::BooleanExpression.new(:AND, self, ce)
|
69
69
|
end
|
70
70
|
|
71
|
-
# Return a Sequel::SQL::
|
71
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, matching
|
72
72
|
# all of the conditions in this hash or the condition specified by
|
73
73
|
# the given argument.
|
74
74
|
def |(ce)
|
75
|
-
::Sequel::SQL::
|
75
|
+
::Sequel::SQL::BooleanExpression.new(:OR, self, ce)
|
76
76
|
end
|
77
77
|
|
78
|
-
# Return a Sequel::SQL::
|
78
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, not matching any of the
|
79
79
|
# conditions.
|
80
80
|
def ~
|
81
|
-
::Sequel::SQL::
|
81
|
+
::Sequel::SQL::BooleanExpression.from_value_pairs(self, :OR, true)
|
82
82
|
end
|
83
83
|
|
84
|
-
# Return a Sequel::SQL::
|
84
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, matching all of the
|
85
85
|
# conditions.
|
86
86
|
def sql_expr
|
87
|
-
::Sequel::SQL::
|
87
|
+
::Sequel::SQL::BooleanExpression.from_value_pairs(self)
|
88
88
|
end
|
89
89
|
|
90
|
-
# Return a Sequel::SQL::
|
90
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, matching none
|
91
91
|
# of the conditions.
|
92
92
|
def sql_negate
|
93
|
-
::Sequel::SQL::
|
93
|
+
::Sequel::SQL::BooleanExpression.from_value_pairs(self, :AND, true)
|
94
94
|
end
|
95
95
|
|
96
|
-
# Return a Sequel::SQL::
|
96
|
+
# Return a Sequel::SQL::BooleanExpression created from this hash, matching any of the
|
97
97
|
# conditions.
|
98
98
|
def sql_or
|
99
|
-
::Sequel::SQL::
|
99
|
+
::Sequel::SQL::BooleanExpression.from_value_pairs(self, :OR)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
@@ -136,7 +136,7 @@ class Symbol
|
|
136
136
|
|
137
137
|
# If no argument is given, returns a Sequel::SQL::ColumnAll object specifying all
|
138
138
|
# columns for this table.
|
139
|
-
# If an argument is given, returns a Sequel::SQL::
|
139
|
+
# If an argument is given, returns a Sequel::SQL::NumericExpression using the *
|
140
140
|
# (multiplication) operator with this and the given argument.
|
141
141
|
def *(ce=(arg=false;nil))
|
142
142
|
return super(ce) unless arg == false
|
@@ -151,7 +151,7 @@ class Symbol
|
|
151
151
|
|
152
152
|
# If the given argument is an Integer or an array containing an Integer, returns
|
153
153
|
# a Sequel::SQL::Subscript with this column and the given arg.
|
154
|
-
# Otherwise returns a Sequel::SQL::
|
154
|
+
# Otherwise returns a Sequel::SQL::BooleanExpression where this column (which should be boolean)
|
155
155
|
# or the given argument is true.
|
156
156
|
def |(sub)
|
157
157
|
return super unless (Integer === sub) || ((Array === sub) && sub.any?{|x| Integer === x})
|
data/lib/sequel_core/database.rb
CHANGED
@@ -396,18 +396,24 @@ module Sequel
|
|
396
396
|
else
|
397
397
|
raise ArgumentError, "invalid value for Date: #{value.inspect}"
|
398
398
|
end
|
399
|
-
when :
|
399
|
+
when :time
|
400
400
|
case value
|
401
|
-
when DateTime
|
402
|
-
value
|
403
|
-
when Date
|
404
|
-
DateTime.new(value.year, value.month, value.day)
|
405
401
|
when Time
|
406
|
-
|
402
|
+
value
|
407
403
|
when String
|
408
|
-
value.
|
404
|
+
value.to_time
|
405
|
+
else
|
406
|
+
raise ArgumentError, "invalid value for Time: #{value.inspect}"
|
407
|
+
end
|
408
|
+
when :datetime
|
409
|
+
raise(ArgumentError, "invalid value for #{tc}: #{value.inspect}") unless value.is_one_of?(DateTime, Date, Time, String)
|
410
|
+
if Sequel.time_class === value
|
411
|
+
# Already the correct class, no need to convert
|
412
|
+
value
|
409
413
|
else
|
410
|
-
|
414
|
+
# First convert it to standard ISO 8601 time, then
|
415
|
+
# parse that string using the time class.
|
416
|
+
(Time === value ? value.iso8601 : value.to_s).to_sequel_time
|
411
417
|
end
|
412
418
|
else
|
413
419
|
value
|
@@ -52,6 +52,8 @@ module Sequel
|
|
52
52
|
"(#{args.collect{|a| literal(a)}.join(" #{op} ")})"
|
53
53
|
when :NOT
|
54
54
|
"NOT #{literal(args.at(0))}"
|
55
|
+
when :NOOP
|
56
|
+
literal(args.at(0))
|
55
57
|
else
|
56
58
|
raise(Sequel::Error, "invalid operator #{op}")
|
57
59
|
end
|
@@ -107,8 +109,8 @@ module Sequel
|
|
107
109
|
cond = cond.first if cond.size == 1
|
108
110
|
cond = cond.sql_or if (Hash === cond) || ((Array === cond) && (cond.all_two_pairs?))
|
109
111
|
cond = filter_expr(block || cond)
|
110
|
-
cond = SQL::
|
111
|
-
cond = SQL::
|
112
|
+
cond = SQL::BooleanExpression.invert(cond)
|
113
|
+
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
112
114
|
clone(clause => cond)
|
113
115
|
end
|
114
116
|
|
@@ -135,7 +137,7 @@ module Sequel
|
|
135
137
|
# specified.
|
136
138
|
# * String - taken literally
|
137
139
|
# * Symbol - taken as a boolean column argument (e.g. WHERE active)
|
138
|
-
# * Sequel::SQL::
|
140
|
+
# * Sequel::SQL::BooleanExpression - an existing condition expression,
|
139
141
|
# probably created using the Sequel blockless filter DSL.
|
140
142
|
#
|
141
143
|
# filter also takes a block, but use of this is discouraged as it requires
|
@@ -169,7 +171,7 @@ module Sequel
|
|
169
171
|
raise(Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?") if cond === true || cond === false
|
170
172
|
cond = transform_save(cond) if @transform if cond.is_a?(Hash)
|
171
173
|
cond = filter_expr(block || cond)
|
172
|
-
cond = SQL::
|
174
|
+
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause] && !@opts[clause].blank?
|
173
175
|
clone(clause => cond)
|
174
176
|
end
|
175
177
|
alias_method :where, :filter
|
@@ -215,11 +217,11 @@ module Sequel
|
|
215
217
|
|
216
218
|
# Pattern match any of the columns to any of the terms. The terms can be
|
217
219
|
# strings (which use LIKE) or regular expressions (which are only supported
|
218
|
-
# in some databases). See Sequel::SQL::
|
220
|
+
# in some databases). See Sequel::SQL::StringExpression.like. Note that the
|
219
221
|
# total number of pattern matches will be cols.length * terms.length,
|
220
222
|
# which could cause performance issues.
|
221
223
|
def grep(cols, terms)
|
222
|
-
filter(SQL::
|
224
|
+
filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
|
223
225
|
end
|
224
226
|
|
225
227
|
# Returns a copy of the dataset with the results grouped by the value of
|
@@ -312,12 +314,8 @@ module Sequel
|
|
312
314
|
having, where = @opts[:having], @opts[:where]
|
313
315
|
raise(Error, "No current filter") unless having || where
|
314
316
|
o = {}
|
315
|
-
if having
|
316
|
-
|
317
|
-
end
|
318
|
-
if where
|
319
|
-
o[:where] = SQL::ComplexExpression === where ? ~where : SQL::ComplexExpression.new(:NOT, where)
|
320
|
-
end
|
317
|
+
o[:having] = SQL::BooleanExpression.invert(having) if having
|
318
|
+
o[:where] = SQL::BooleanExpression.invert(where) if where
|
321
319
|
clone(o)
|
322
320
|
end
|
323
321
|
|
@@ -451,7 +449,7 @@ module Sequel
|
|
451
449
|
clause = (@opts[:having] ? :having : :where)
|
452
450
|
cond = cond.first if cond.size == 1
|
453
451
|
if @opts[clause]
|
454
|
-
clone(clause => SQL::
|
452
|
+
clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(block || cond)))
|
455
453
|
else
|
456
454
|
raise Error::NoExistingFilter, "No existing filter found."
|
457
455
|
end
|
@@ -494,11 +492,17 @@ module Sequel
|
|
494
492
|
end
|
495
493
|
alias_method :quote_column_ref, :quote_identifier
|
496
494
|
|
497
|
-
# This method quotes the given name with the SQL standard double quote.
|
495
|
+
# This method quotes the given name with the SQL standard double quote.
|
496
|
+
# It uppercases the name given to conform with the SQL standard. This
|
498
497
|
# should be overridden by subclasses to provide quoting not matching the
|
499
|
-
# SQL standard, such as backtick (used by MySQL and SQLite)
|
498
|
+
# SQL standard, such as backtick (used by MySQL and SQLite), or where
|
499
|
+
# lowercase is the default for unquoted identifiers (PostgreSQL).
|
500
|
+
#
|
501
|
+
# If you are using a database such as Oracle that defaults to uppercase
|
502
|
+
# but you are using lower case identifiers, you should override this
|
503
|
+
# method to not upcase the name for those identifiers.
|
500
504
|
def quoted_identifier(name)
|
501
|
-
"\"#{name}\""
|
505
|
+
"\"#{name.to_s.upcase}\""
|
502
506
|
end
|
503
507
|
|
504
508
|
# Returns a copy of the dataset with the order reversed. If no order is
|
@@ -710,21 +714,23 @@ module Sequel
|
|
710
714
|
def filter_expr(expr)
|
711
715
|
case expr
|
712
716
|
when Hash
|
713
|
-
SQL::
|
717
|
+
SQL::BooleanExpression.from_value_pairs(expr)
|
714
718
|
when Array
|
715
719
|
if String === expr[0]
|
716
720
|
filter_expr(expr.shift.gsub(QUESTION_MARK){literal(expr.shift)}.lit)
|
717
721
|
else
|
718
|
-
SQL::
|
722
|
+
SQL::BooleanExpression.from_value_pairs(expr)
|
719
723
|
end
|
720
724
|
when Proc
|
721
725
|
expr.to_sql(self).lit
|
726
|
+
when SQL::NumericExpression, SQL::StringExpression
|
727
|
+
raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
|
722
728
|
when Symbol, SQL::Expression
|
723
729
|
expr
|
724
730
|
when String
|
725
731
|
"(#{expr})".lit
|
726
732
|
else
|
727
|
-
raise(
|
733
|
+
raise(Error, 'Invalid filter argument')
|
728
734
|
end
|
729
735
|
end
|
730
736
|
|
@@ -231,10 +231,12 @@ module Sequel
|
|
231
231
|
:integer
|
232
232
|
when /\A(character( varying)?|varchar|text)\z/
|
233
233
|
:string
|
234
|
-
when /\
|
234
|
+
when /\Adate\z/
|
235
235
|
:date
|
236
|
-
when /\A(datetime|
|
236
|
+
when /\A(datetime|timestamp( with(out)? time zone)?)\z/
|
237
237
|
:datetime
|
238
|
+
when /\Atime( with(out)? time zone)?\z/
|
239
|
+
:time
|
238
240
|
when /\A(boolean|tinyint)\z/
|
239
241
|
:boolean
|
240
242
|
when /\A(real|float|double( precision)?)\z/
|
data/lib/sequel_core/sql.rb
CHANGED
@@ -6,11 +6,38 @@ module Sequel
|
|
6
6
|
# It also holds modules that are included in core ruby classes that
|
7
7
|
# make Sequel a friendly DSL.
|
8
8
|
module SQL
|
9
|
-
|
10
|
-
|
9
|
+
# Holds methods that should be called on columns only.
|
10
|
+
module ColumnMethods
|
11
|
+
AS = 'AS'.freeze
|
12
|
+
DESC = 'DESC'.freeze
|
13
|
+
ASC = 'ASC'.freeze
|
14
|
+
|
15
|
+
# Create an SQL column alias of the receiving column to the given alias.
|
16
|
+
def as(a)
|
17
|
+
ColumnExpr.new(self, AS, a)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Mark the receiving SQL column as sorting in a descending fashion.
|
21
|
+
def desc
|
22
|
+
ColumnExpr.new(self, DESC)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Mark the receiving SQL column as sorting in an ascending fashion (generally a no-op).
|
26
|
+
def asc
|
27
|
+
ColumnExpr.new(self, ASC)
|
28
|
+
end
|
11
29
|
|
30
|
+
# Cast the reciever to the given SQL type
|
31
|
+
def cast_as(t)
|
32
|
+
t = t.to_s.lit if t.is_a?(Symbol)
|
33
|
+
Sequel::SQL::Function.new(:cast, self.as(t))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
12
37
|
# Base class for all SQL fragments
|
13
38
|
class Expression
|
39
|
+
include ColumnMethods
|
40
|
+
|
14
41
|
# Returns self, because SQL::Expression already acts like
|
15
42
|
# LiteralString.
|
16
43
|
def lit
|
@@ -67,52 +94,44 @@ module Sequel
|
|
67
94
|
# a tree). This class is the backbone of the blockless filter support in
|
68
95
|
# Sequel.
|
69
96
|
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
97
|
+
# This is an abstract class that is not that useful by itself. The
|
98
|
+
# subclasses BooleanExpression, NumericExpression, and StringExpression
|
99
|
+
# define the behavior of the DSL via operators.
|
73
100
|
class ComplexExpression < Expression
|
74
|
-
# A hash of the opposite for each operator symbol, used for inverting
|
101
|
+
# A hash of the opposite for each operator symbol, used for inverting
|
75
102
|
# objects.
|
76
103
|
OPERTATOR_INVERSIONS = {:AND => :OR, :OR => :AND, :< => :>=, :> => :<=,
|
77
104
|
:<= => :>, :>= => :<, :'=' => :'!=' , :'!=' => :'=', :LIKE => :'NOT LIKE',
|
78
105
|
:'NOT LIKE' => :LIKE, :~ => :'!~', :'!~' => :~, :IN => :'NOT IN',
|
79
106
|
:'NOT IN' => :IN, :IS => :'IS NOT', :'IS NOT' => :IS, :'~*' => :'!~*',
|
80
|
-
:'!~*' => :'~*'
|
107
|
+
:'!~*' => :'~*', :NOT => :NOOP, :NOOP => :NOT, :ILIKE => :'NOT ILIKE',
|
108
|
+
:'NOT ILIKE'=>:ILIKE}
|
81
109
|
|
110
|
+
# Mathematical Operators used in NumericMethods
|
82
111
|
MATHEMATICAL_OPERATORS = [:+, :-, :/, :*]
|
83
|
-
INEQUALITY_OPERATORS = [:<, :>, :<=, :>=]
|
84
|
-
STRING_OPERATORS = [:'||']
|
85
|
-
SEARCH_OPERATORS = [:LIKE, :'NOT LIKE', :~, :'!~', :'~*', :'!~*']
|
86
|
-
INCLUSION_OPERATORS = [:IN, :'NOT IN']
|
87
|
-
BOOLEAN_OPERATORS = [:AND, :OR]
|
88
|
-
|
89
|
-
# Collection of all equality/inequality operator symbols
|
90
|
-
EQUALITY_OPERATORS = [:'=', :'!=', :IS, :'IS NOT', *INEQUALITY_OPERATORS]
|
91
|
-
|
92
|
-
# Operator symbols that do not work on boolean SQL input
|
93
|
-
NO_BOOLEAN_INPUT_OPERATORS = MATHEMATICAL_OPERATORS + INEQUALITY_OPERATORS + STRING_OPERATORS
|
94
112
|
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
# Literal SQL booleans that are not allowed
|
99
|
-
BOOLEAN_LITERALS = [true, false, nil]
|
113
|
+
# Inequality Operators used in InequalityMethods
|
114
|
+
INEQUALITY_OPERATORS = [:<, :>, :<=, :>=]
|
100
115
|
|
101
|
-
# Hash of ruby operator symbols to SQL operators, used
|
116
|
+
# Hash of ruby operator symbols to SQL operators, used in BooleanMethods
|
102
117
|
BOOLEAN_OPERATOR_METHODS = {:& => :AND, :| =>:OR}
|
103
118
|
|
104
119
|
# Operator symbols that take exactly two arguments
|
105
|
-
TWO_ARITY_OPERATORS =
|
120
|
+
TWO_ARITY_OPERATORS = [:'=', :'!=', :IS, :'IS NOT', :LIKE, :'NOT LIKE', \
|
121
|
+
:~, :'!~', :'~*', :'!~*', :IN, :'NOT IN', :ILIKE, :'NOT ILIKE'] + INEQUALITY_OPERATORS
|
106
122
|
|
107
123
|
# Operator symbols that take one or more arguments
|
108
|
-
N_ARITY_OPERATORS =
|
124
|
+
N_ARITY_OPERATORS = [:AND, :OR, :'||'] + MATHEMATICAL_OPERATORS
|
125
|
+
|
126
|
+
# Operator symbols that take one argument
|
127
|
+
ONE_ARITY_OPERATORS = [:NOT, :NOOP]
|
109
128
|
|
110
129
|
# An array of args for this object
|
111
130
|
attr_reader :args
|
112
131
|
|
113
132
|
# The operator symbol for this object
|
114
133
|
attr_reader :op
|
115
|
-
|
134
|
+
|
116
135
|
# Set the operator symbol and arguments for this object to the ones given.
|
117
136
|
# Convert all args that are hashes or arrays with all two pairs to ComplexExpressions.
|
118
137
|
# Raise an error if the operator doesn't allow boolean input and a boolean argument is given.
|
@@ -128,31 +147,184 @@ module Sequel
|
|
128
147
|
a
|
129
148
|
end
|
130
149
|
end
|
131
|
-
if NO_BOOLEAN_INPUT_OPERATORS.include?(op)
|
132
|
-
args.each do |a|
|
133
|
-
if BOOLEAN_LITERALS.include?(a) || ((ComplexExpression === a) && BOOLEAN_RESULT_OPERATORS.include?(a.op))
|
134
|
-
raise(Sequel::Error, "cannot apply #{op} to a boolean expression")
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
150
|
case op
|
139
151
|
when *N_ARITY_OPERATORS
|
140
|
-
raise(
|
152
|
+
raise(Error, "The #{op} operator requires at least 1 argument") unless args.length >= 1
|
141
153
|
when *TWO_ARITY_OPERATORS
|
142
|
-
raise(
|
143
|
-
when
|
144
|
-
raise(
|
154
|
+
raise(Error, "The #{op} operator requires precisely 2 arguments") unless args.length == 2
|
155
|
+
when *ONE_ARITY_OPERATORS
|
156
|
+
raise(Error, "The #{op} operator requires a single argument") unless args.length == 1
|
145
157
|
else
|
146
|
-
raise(
|
158
|
+
raise(Error, "Invalid operator #{op}")
|
147
159
|
end
|
148
160
|
@op = op
|
149
161
|
@args = args
|
150
162
|
end
|
163
|
+
|
164
|
+
# Delegate the creation of the resulting SQL to the given dataset,
|
165
|
+
# since it may be database dependent.
|
166
|
+
def to_s(ds)
|
167
|
+
ds.complex_expression_sql(@op, @args)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# This module includes the methods that are defined on objects that can be
|
172
|
+
# used in a boolean context in SQL (Symbol, LiteralString, SQL::Function,
|
173
|
+
# and SQL::BooleanExpression).
|
174
|
+
#
|
175
|
+
# This defines the ~ (NOT), & (AND), and | (OR) methods.
|
176
|
+
module BooleanMethods
|
177
|
+
# Create a new BooleanExpression with NOT, representing the inversion of whatever self represents.
|
178
|
+
def ~
|
179
|
+
BooleanExpression.invert(self)
|
180
|
+
end
|
181
|
+
|
182
|
+
ComplexExpression::BOOLEAN_OPERATOR_METHODS.each do |m, o|
|
183
|
+
define_method(m) do |ce|
|
184
|
+
case ce
|
185
|
+
when BooleanExpression
|
186
|
+
BooleanExpression.new(o, self, ce)
|
187
|
+
when ComplexExpression
|
188
|
+
raise(Sequel::Error, "cannot apply #{o} to a non-boolean expression")
|
189
|
+
else
|
190
|
+
BooleanExpression.new(o, self, ce)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# This module includes the methods that are defined on objects that can be
|
197
|
+
# used in a numeric context in SQL (Symbol, LiteralString, SQL::Function,
|
198
|
+
# and SQL::NumericExpression).
|
199
|
+
#
|
200
|
+
# This defines the +, -, *, and / methods.
|
201
|
+
module NumericMethods
|
202
|
+
ComplexExpression::MATHEMATICAL_OPERATORS.each do |o|
|
203
|
+
define_method(o) do |ce|
|
204
|
+
case ce
|
205
|
+
when NumericExpression
|
206
|
+
NumericExpression.new(o, self, ce)
|
207
|
+
when ComplexExpression
|
208
|
+
raise(Sequel::Error, "cannot apply #{o} to a non-numeric expression")
|
209
|
+
else
|
210
|
+
NumericExpression.new(o, self, ce)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# This module includes the methods that are defined on objects that can be
|
217
|
+
# used in a numeric context in SQL (Symbol, LiteralString, SQL::Function,
|
218
|
+
# and SQL::StringExpression).
|
219
|
+
#
|
220
|
+
# This defines the like (LIKE) method, used for pattern matching.
|
221
|
+
module StringMethods
|
222
|
+
# Create a BooleanExpression case insensitive pattern match of self
|
223
|
+
# with the given patterns. See StringExpression.like.
|
224
|
+
def ilike(*ces)
|
225
|
+
StringExpression.like(self, *(ces << {:case_insensitive=>true}))
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create a BooleanExpression case sensitive pattern match of self with
|
229
|
+
# the given patterns. See StringExpression.like.
|
230
|
+
def like(*ces)
|
231
|
+
StringExpression.like(self, *ces)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# This module is included in StringExpression and can be included elsewhere
|
236
|
+
# to allow the use of the + operator to represent concatenation of SQL
|
237
|
+
# Strings:
|
238
|
+
#
|
239
|
+
# :x.sql_string + :y => # SQL: x || y
|
240
|
+
module StringConcatenationMethods
|
241
|
+
def +(ce)
|
242
|
+
StringExpression.new(:'||', self, ce)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# This module includes the methods that are defined on objects that can be
|
247
|
+
# used in a numeric or string context in SQL (Symbol, LiteralString,
|
248
|
+
# SQL::Function, and SQL::StringExpression).
|
249
|
+
#
|
250
|
+
# This defines the >, <, >=, and <= methods.
|
251
|
+
module InequalityMethods
|
252
|
+
ComplexExpression::INEQUALITY_OPERATORS.each do |o|
|
253
|
+
define_method(o) do |ce|
|
254
|
+
case ce
|
255
|
+
when BooleanExpression, TrueClass, FalseClass, NilClass, Hash, Array
|
256
|
+
raise(Error, "cannot apply #{o} to a boolean expression")
|
257
|
+
else
|
258
|
+
BooleanExpression.new(o, self, ce)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# This module augments the default initalize method for the
|
265
|
+
# ComplexExpression subclass it is included in, so that
|
266
|
+
# attempting to use boolean input when initializing a NumericExpression
|
267
|
+
# or StringExpression results in an error.
|
268
|
+
module NoBooleanInputMethods
|
269
|
+
# Raise an Error if one of the args would be boolean in an SQL
|
270
|
+
# context, otherwise call super.
|
271
|
+
def initialize(op, *args)
|
272
|
+
args.each do |a|
|
273
|
+
case a
|
274
|
+
when BooleanExpression, TrueClass, FalseClass, NilClass, Hash, Array
|
275
|
+
raise(Error, "cannot apply #{op} to a boolean expression")
|
276
|
+
end
|
277
|
+
end
|
278
|
+
super
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# This module includes other Sequel::SQL::*Methods modules and is
|
283
|
+
# included in other classes that are could be either booleans,
|
284
|
+
# strings, or numbers. It also adds three methods so that
|
285
|
+
# can specify behavior in case one of the operator methods has
|
286
|
+
# been overridden (such as Symbol#/).
|
287
|
+
#
|
288
|
+
# For example, if Symbol#/ is overridden to produce a string (for
|
289
|
+
# example, to make file system path creation easier), the
|
290
|
+
# following code will not do what you want:
|
291
|
+
#
|
292
|
+
# :price/10 > 100
|
293
|
+
#
|
294
|
+
# In that case, you need to do the following:
|
295
|
+
#
|
296
|
+
# :price.sql_number/10 > 100
|
297
|
+
module ComplexExpressionMethods
|
298
|
+
include BooleanMethods
|
299
|
+
include NumericMethods
|
300
|
+
include StringMethods
|
301
|
+
include InequalityMethods
|
302
|
+
|
303
|
+
# Return a BooleanExpression representation of self.
|
304
|
+
def sql_boolean
|
305
|
+
BooleanExpression.new(:NOOP, self)
|
306
|
+
end
|
151
307
|
|
308
|
+
# Return a NumericExpression representation of self.
|
309
|
+
def sql_number
|
310
|
+
NumericExpression.new(:NOOP, self)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Return a StringExpression representation of self.
|
314
|
+
def sql_string
|
315
|
+
StringExpression.new(:NOOP, self)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Subclass of ComplexExpression where the expression results
|
320
|
+
# in a boolean value in SQL.
|
321
|
+
class BooleanExpression < ComplexExpression
|
322
|
+
include BooleanMethods
|
323
|
+
|
152
324
|
# Take pairs of values (e.g. a hash or array of arrays of two pairs)
|
153
325
|
# and converts it to a ComplexExpression. The operator and args
|
154
326
|
# used depends on the case of the right (2nd) argument:
|
155
|
-
#
|
327
|
+
#
|
156
328
|
# * 0..10 - left >= 0 AND left <= 10
|
157
329
|
# * [1,2] - left IN (1,2)
|
158
330
|
# * nil - left IS NULL
|
@@ -177,15 +349,51 @@ module Sequel
|
|
177
349
|
when NilClass
|
178
350
|
new(:IS, l, r)
|
179
351
|
when Regexp
|
180
|
-
like(l, r)
|
352
|
+
StringExpression.like(l, r)
|
181
353
|
else
|
182
354
|
new(:'=', l, r)
|
183
355
|
end
|
184
|
-
negate ?
|
356
|
+
negate ? invert(ce) : ce
|
185
357
|
end
|
186
358
|
pairs.length == 1 ? pairs.at(0) : new(op, *pairs)
|
187
359
|
end
|
360
|
+
|
361
|
+
# Invert the expression, if possible. If the expression cannot
|
362
|
+
# be inverted, raise an error. An inverted expression should match everything that the
|
363
|
+
# uninverted expression did not match, and vice-versa.
|
364
|
+
def self.invert(ce)
|
365
|
+
case ce
|
366
|
+
when BooleanExpression
|
367
|
+
case op = ce.op
|
368
|
+
when :AND, :OR
|
369
|
+
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.collect{|a| BooleanExpression.invert(a)})
|
370
|
+
else
|
371
|
+
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.dup)
|
372
|
+
end
|
373
|
+
when ComplexExpression
|
374
|
+
raise(Sequel::Error, "operator #{ce.op} cannot be inverted")
|
375
|
+
else
|
376
|
+
BooleanExpression.new(:NOT, ce)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Subclass of ComplexExpression where the expression results
|
382
|
+
# in a numeric value in SQL.
|
383
|
+
class NumericExpression < ComplexExpression
|
384
|
+
include NumericMethods
|
385
|
+
include InequalityMethods
|
386
|
+
include NoBooleanInputMethods
|
387
|
+
end
|
188
388
|
|
389
|
+
# Subclass of ComplexExpression where the expression results
|
390
|
+
# in a text/string/varchar value in SQL.
|
391
|
+
class StringExpression < ComplexExpression
|
392
|
+
include StringMethods
|
393
|
+
include StringConcatenationMethods
|
394
|
+
include InequalityMethods
|
395
|
+
include NoBooleanInputMethods
|
396
|
+
|
189
397
|
# Creates a SQL pattern match exprssion. left (l) is the SQL string we
|
190
398
|
# are matching against, and ces are the patterns we are matching.
|
191
399
|
# The match succeeds if any of the patterns match (SQL OR). Patterns
|
@@ -193,55 +401,26 @@ module Sequel
|
|
193
401
|
# the SQL LIKE operator to be used, and should be supported by most
|
194
402
|
# databases. Regular expressions will probably only work on MySQL
|
195
403
|
# and PostgreSQL, and SQL regular expression syntax is not fully compatible
|
196
|
-
# with ruby regular expression syntax, so be careful if using regular
|
404
|
+
# with ruby regular expression syntax, so be careful if using regular
|
197
405
|
# expressions.
|
406
|
+
#
|
407
|
+
# The pattern match will be case insensitive if the last argument is a hash
|
408
|
+
# with a key of :case_insensitive that is not false or nil. Also,
|
409
|
+
# if a case insensitive regular expression is used (//i), that particular
|
410
|
+
# pattern which will always be case insensitive.
|
198
411
|
def self.like(l, *ces)
|
199
|
-
ces.
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
ces.length == 1 ? ces.at(0) : new(:OR, *ces)
|
204
|
-
end
|
205
|
-
|
206
|
-
# Invert the regular expression, if possible. If the expression cannot
|
207
|
-
# be inverted, raise an error. An inverted expression should match everything that the
|
208
|
-
# uninverted expression did not match, and vice-versa.
|
209
|
-
def ~
|
210
|
-
case @op
|
211
|
-
when *BOOLEAN_OPERATORS
|
212
|
-
self.class.new(OPERTATOR_INVERSIONS[@op], *@args.collect{|a| ComplexExpression === a ? ~a : ComplexExpression.new(:NOT, a)})
|
213
|
-
when *TWO_ARITY_OPERATORS
|
214
|
-
self.class.new(OPERTATOR_INVERSIONS[@op], *@args.dup)
|
215
|
-
when :NOT
|
216
|
-
@args.first
|
217
|
-
else
|
218
|
-
raise(Sequel::Error, "operator #{@op} cannot be inverted")
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# Delegate the creation of the resulting SQL to the given dataset,
|
223
|
-
# since it may be database dependent.
|
224
|
-
def to_s(ds)
|
225
|
-
ds.complex_expression_sql(@op, @args)
|
226
|
-
end
|
227
|
-
|
228
|
-
BOOLEAN_OPERATOR_METHODS.each do |m, o|
|
229
|
-
define_method(m) do |ce|
|
230
|
-
raise(Sequel::Error, "cannot apply #{o} to a non-boolean expression") unless BOOLEAN_RESULT_OPERATORS.include?(@op)
|
231
|
-
super
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
(MATHEMATICAL_OPERATORS + INEQUALITY_OPERATORS).each do |o|
|
236
|
-
define_method(o) do |ce|
|
237
|
-
raise(Sequel::Error, "cannot apply #{o} to a boolean expression") unless NO_BOOLEAN_INPUT_OPERATORS.include?(@op)
|
238
|
-
super
|
412
|
+
case_insensitive = ces.extract_options![:case_insensitive]
|
413
|
+
ces.collect! do |ce|
|
414
|
+
op, expr = Regexp === ce ? [ce.casefold? || case_insensitive ? :'~*' : :~, ce.source] : [case_insensitive ? :ILIKE : :LIKE, ce.to_s]
|
415
|
+
BooleanExpression.new(op, l, expr)
|
239
416
|
end
|
417
|
+
ces.length == 1 ? ces.at(0) : BooleanExpression.new(:OR, *ces)
|
240
418
|
end
|
241
419
|
end
|
242
420
|
|
243
421
|
# Represents an SQL function call.
|
244
422
|
class Function < Expression
|
423
|
+
include ComplexExpressionMethods
|
245
424
|
# The array of arguments to pass to the function (may be blank)
|
246
425
|
attr_reader :args
|
247
426
|
|
@@ -309,78 +488,6 @@ module Sequel
|
|
309
488
|
ds.subscript_sql(self)
|
310
489
|
end
|
311
490
|
end
|
312
|
-
|
313
|
-
### Modules ###
|
314
|
-
|
315
|
-
# Module included in core classes giving them a simple and easy DSL
|
316
|
-
# for creation of ComplexExpressions.
|
317
|
-
#
|
318
|
-
# Most ruby operators methods are defined via metaprogramming: +, -, /, *, <, >, <=,
|
319
|
-
# >=, & (AND), | (OR).
|
320
|
-
module ComplexExpressionMethods
|
321
|
-
NO_BOOLEAN_INPUT_OPERATORS = ComplexExpression::NO_BOOLEAN_INPUT_OPERATORS
|
322
|
-
BOOLEAN_RESULT_OPERATORS = ComplexExpression::BOOLEAN_RESULT_OPERATORS
|
323
|
-
BOOLEAN_OPERATOR_METHODS = ComplexExpression::BOOLEAN_OPERATOR_METHODS
|
324
|
-
|
325
|
-
BOOLEAN_OPERATOR_METHODS.each do |m, o|
|
326
|
-
define_method(m) do |ce|
|
327
|
-
raise(Sequel::Error, "cannot apply #{o} to a non-boolean expression") if (ComplexExpression === ce) && !BOOLEAN_RESULT_OPERATORS.include?(ce.op)
|
328
|
-
ComplexExpression.new(o, self, ce)
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
(ComplexExpression::MATHEMATICAL_OPERATORS + ComplexExpression::INEQUALITY_OPERATORS).each do |o|
|
333
|
-
define_method(o) do |ce|
|
334
|
-
raise(Sequel::Error, "cannot apply #{o} to a boolean expression") if (ComplexExpression === ce) && !NO_BOOLEAN_INPUT_OPERATORS.include?(ce.op)
|
335
|
-
ComplexExpression.new(o, self, ce)
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
# Create a new ComplexExpression with NOT, representing the inversion of whatever self represents.
|
340
|
-
def ~
|
341
|
-
ComplexExpression.new(:NOT, self)
|
342
|
-
end
|
343
|
-
|
344
|
-
# Create a ComplexExpression pattern match of self with the given patterns.
|
345
|
-
def like(*ces)
|
346
|
-
ComplexExpression.like(self, *ces)
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
# Holds methods that should be called on columns only.
|
351
|
-
module ColumnMethods
|
352
|
-
AS = 'AS'.freeze
|
353
|
-
DESC = 'DESC'.freeze
|
354
|
-
ASC = 'ASC'.freeze
|
355
|
-
|
356
|
-
# Create an SQL column alias of the receiving column to the given alias.
|
357
|
-
def as(a)
|
358
|
-
ColumnExpr.new(self, AS, a)
|
359
|
-
end
|
360
|
-
|
361
|
-
# Mark the receiving SQL column as sorting in a descending fashion.
|
362
|
-
def desc
|
363
|
-
ColumnExpr.new(self, DESC)
|
364
|
-
end
|
365
|
-
|
366
|
-
# Mark the receiving SQL column as sorting in an ascending fashion (generally a no-op).
|
367
|
-
def asc
|
368
|
-
ColumnExpr.new(self, ASC)
|
369
|
-
end
|
370
|
-
|
371
|
-
# Cast the reciever to the given SQL type
|
372
|
-
def cast_as(t)
|
373
|
-
t = t.to_s.lit if t.is_a?(Symbol)
|
374
|
-
Sequel::SQL::Function.new(:cast, self.as(t))
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
class Expression
|
379
|
-
# Include the modules in Expression, couldn't be done
|
380
|
-
# earlier due to cyclic dependencies.
|
381
|
-
include ColumnMethods
|
382
|
-
include ComplexExpressionMethods
|
383
|
-
end
|
384
491
|
end
|
385
492
|
|
386
493
|
# LiteralString is used to represent literal SQL expressions. An
|
data/lib/sequel_core.rb
CHANGED
@@ -21,7 +21,15 @@ end
|
|
21
21
|
# object, which is closed when the block exits. For example:
|
22
22
|
#
|
23
23
|
# Sequel.sqlite('blog.db'){|db| puts db.users.count}
|
24
|
+
#
|
25
|
+
# Sequel can use either Time or DateTime for times returned from the
|
26
|
+
# database. It defaults to Time. To change it to DateTime, use:
|
27
|
+
#
|
28
|
+
# Sequel.time_class = DateTime
|
24
29
|
module Sequel
|
30
|
+
@time_class = Time
|
31
|
+
metaattr_accessor :time_class
|
32
|
+
|
25
33
|
# Creates a new database object based on the supplied connection string
|
26
34
|
# and optional arguments. The specified scheme determines the database
|
27
35
|
# class used, and the rest of the string specifies the connection options.
|
data/spec/adapters/mysql_spec.rb
CHANGED
@@ -713,14 +713,15 @@ context "MySQL::Dataset#complex_expression_sql" do
|
|
713
713
|
@d = MYSQL_DB.dataset
|
714
714
|
end
|
715
715
|
|
716
|
-
specify "should handle
|
717
|
-
@d.literal(:x.like(
|
718
|
-
@d.literal(~:x.like(
|
719
|
-
|
720
|
-
|
721
|
-
specify "should handle case sensitive regular expressions with REGEXP BINARY" do
|
716
|
+
specify "should handle pattern matches correctly" do
|
717
|
+
@d.literal(:x.like('a')).should == "(x LIKE BINARY 'a')"
|
718
|
+
@d.literal(~:x.like('a')).should == "(x NOT LIKE BINARY 'a')"
|
719
|
+
@d.literal(:x.ilike('a')).should == "(x LIKE 'a')"
|
720
|
+
@d.literal(~:x.ilike('a')).should == "(x NOT LIKE 'a')"
|
722
721
|
@d.literal(:x.like(/a/)).should == "(x REGEXP BINARY 'a')"
|
723
|
-
@d.literal(~:x.like(/a/)).should == "
|
722
|
+
@d.literal(~:x.like(/a/)).should == "(x NOT REGEXP BINARY 'a')"
|
723
|
+
@d.literal(:x.like(/a/i)).should == "(x REGEXP 'a')"
|
724
|
+
@d.literal(~:x.like(/a/i)).should == "(x NOT REGEXP 'a')"
|
724
725
|
end
|
725
726
|
|
726
727
|
specify "should handle string concatenation with CONCAT if more than one record" do
|
@@ -256,6 +256,20 @@ context "An SQLite dataset" do
|
|
256
256
|
@d.count.should == 1
|
257
257
|
@d.first[:name].should == 'def'
|
258
258
|
end
|
259
|
+
|
260
|
+
specify "should handle string pattern matches correctly" do
|
261
|
+
@d.literal(:x.like('a')).should == "(x LIKE 'a')"
|
262
|
+
@d.literal(~:x.like('a')).should == "NOT (x LIKE 'a')"
|
263
|
+
@d.literal(:x.ilike('a')).should == "(x LIKE 'a')"
|
264
|
+
@d.literal(~:x.ilike('a')).should == "NOT (x LIKE 'a')"
|
265
|
+
end
|
266
|
+
|
267
|
+
specify "should raise errors if given a regexp pattern match" do
|
268
|
+
proc{@d.literal(:x.like(/a/))}.should raise_error(Sequel::Error)
|
269
|
+
proc{@d.literal(~:x.like(/a/))}.should raise_error(Sequel::Error)
|
270
|
+
proc{@d.literal(:x.like(/a/i))}.should raise_error(Sequel::Error)
|
271
|
+
proc{@d.literal(~:x.like(/a/i))}.should raise_error(Sequel::Error)
|
272
|
+
end
|
259
273
|
end
|
260
274
|
|
261
275
|
context "An SQLite dataset" do
|
@@ -356,38 +370,32 @@ context "SQLite dataset" do
|
|
356
370
|
end
|
357
371
|
end
|
358
372
|
|
359
|
-
__END__
|
360
|
-
|
361
373
|
context "A SQLite database" do
|
362
374
|
setup do
|
363
375
|
@db = SQLITE_DB
|
376
|
+
@db.create_table! :test2 do
|
377
|
+
text :name
|
378
|
+
integer :value
|
379
|
+
end
|
364
380
|
end
|
365
381
|
|
366
382
|
specify "should support add_column operations" do
|
367
383
|
@db.add_column :test2, :xyz, :text
|
368
384
|
|
369
385
|
@db[:test2].columns.should == [:name, :value, :xyz]
|
370
|
-
@db[:test2] << {:name => 'mmm', :value => 111}
|
371
|
-
@db[:test2].first
|
386
|
+
@db[:test2] << {:name => 'mmm', :value => 111, :xyz=>'000'}
|
387
|
+
@db[:test2].first.should == {:name => 'mmm', :value => 111, :xyz=>'000'}
|
372
388
|
end
|
373
389
|
|
374
390
|
specify "should not support drop_column operations" do
|
375
|
-
proc {@db.drop_column :test2, :
|
391
|
+
proc {@db.drop_column :test2, :value}.should raise_error(Sequel::Error)
|
376
392
|
end
|
377
393
|
|
378
394
|
specify "should not support rename_column operations" do
|
379
|
-
@db
|
380
|
-
@db.add_column :test2, :xyz, :text, :default => '000'
|
381
|
-
@db[:test2] << {:name => 'mmm', :value => 111, :xyz => 'qqqq'}
|
382
|
-
|
383
|
-
@db[:test2].columns.should == [:name, :value, :xyz]
|
384
|
-
proc {@db.rename_column :test2, :xyz, :zyx}.should raise_error(Sequel::Error)
|
395
|
+
proc {@db.rename_column :test2, :value, :zyx}.should raise_error(Sequel::Error)
|
385
396
|
end
|
386
397
|
|
387
398
|
specify "should not support set_column_type operations" do
|
388
|
-
@db.
|
389
|
-
@db[:test2].delete
|
390
|
-
@db[:test2] << {:name => 'mmm', :value => 111, :xyz => 56.78}
|
391
|
-
proc {@db.set_column_type :test2, :xyz, :integer}.should raise_error(Sequel::Error)
|
399
|
+
proc {@db.set_column_type :test2, :value, :integer}.should raise_error(Sequel::Error)
|
392
400
|
end
|
393
401
|
end
|
@@ -8,6 +8,9 @@ context "Blockless Ruby Filters" do
|
|
8
8
|
def @d.l(*args)
|
9
9
|
literal(filter_expr(*args))
|
10
10
|
end
|
11
|
+
def @d.lit(*args)
|
12
|
+
literal(*args)
|
13
|
+
end
|
11
14
|
end
|
12
15
|
|
13
16
|
it "should support boolean columns directly" do
|
@@ -81,7 +84,7 @@ context "Blockless Ruby Filters" do
|
|
81
84
|
@d.l(:x.like('a')).should == '(x LIKE \'a\')'
|
82
85
|
@d.l(:x.like(/a/)).should == '(x ~ \'a\')'
|
83
86
|
@d.l(:x.like('a', 'b')).should == '((x LIKE \'a\') OR (x LIKE \'b\'))'
|
84
|
-
@d.l(:x.like(/a/, /b/)).should == '((x ~ \'a\') OR (x
|
87
|
+
@d.l(:x.like(/a/, /b/i)).should == '((x ~ \'a\') OR (x ~* \'b\'))'
|
85
88
|
@d.l(:x.like('a', /b/)).should == '((x LIKE \'a\') OR (x ~ \'b\'))'
|
86
89
|
end
|
87
90
|
|
@@ -89,10 +92,26 @@ context "Blockless Ruby Filters" do
|
|
89
92
|
@d.l(~:x.like('a')).should == '(x NOT LIKE \'a\')'
|
90
93
|
@d.l(~:x.like(/a/)).should == '(x !~ \'a\')'
|
91
94
|
@d.l(~:x.like('a', 'b')).should == '((x NOT LIKE \'a\') AND (x NOT LIKE \'b\'))'
|
92
|
-
@d.l(~:x.like(/a/, /b/)).should == '((x !~ \'a\') AND (x
|
95
|
+
@d.l(~:x.like(/a/, /b/i)).should == '((x !~ \'a\') AND (x !~* \'b\'))'
|
93
96
|
@d.l(~:x.like('a', /b/)).should == '((x NOT LIKE \'a\') AND (x !~ \'b\'))'
|
94
97
|
end
|
95
98
|
|
99
|
+
it "should support ILIKE via Symbol#ilike" do
|
100
|
+
@d.l(:x.ilike('a')).should == '(x ILIKE \'a\')'
|
101
|
+
@d.l(:x.ilike(/a/)).should == '(x ~* \'a\')'
|
102
|
+
@d.l(:x.ilike('a', 'b')).should == '((x ILIKE \'a\') OR (x ILIKE \'b\'))'
|
103
|
+
@d.l(:x.ilike(/a/, /b/i)).should == '((x ~* \'a\') OR (x ~* \'b\'))'
|
104
|
+
@d.l(:x.ilike('a', /b/)).should == '((x ILIKE \'a\') OR (x ~* \'b\'))'
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should support NOT ILIKE via Symbol#ilike and Symbol#~" do
|
108
|
+
@d.l(~:x.ilike('a')).should == '(x NOT ILIKE \'a\')'
|
109
|
+
@d.l(~:x.ilike(/a/)).should == '(x !~* \'a\')'
|
110
|
+
@d.l(~:x.ilike('a', 'b')).should == '((x NOT ILIKE \'a\') AND (x NOT ILIKE \'b\'))'
|
111
|
+
@d.l(~:x.ilike(/a/, /b/i)).should == '((x !~* \'a\') AND (x !~* \'b\'))'
|
112
|
+
@d.l(~:x.ilike('a', /b/)).should == '((x NOT ILIKE \'a\') AND (x !~* \'b\'))'
|
113
|
+
end
|
114
|
+
|
96
115
|
it "should support negating ranges via Hash#~ and Range" do
|
97
116
|
@d.l(~{:x => 1..5}).should == '((x < 1) OR (x > 5))'
|
98
117
|
@d.l(~{:x => 1...5}).should == '((x < 1) OR (x >= 5))'
|
@@ -113,7 +132,10 @@ context "Blockless Ruby Filters" do
|
|
113
132
|
|
114
133
|
it "should not allow negation of non-boolean expressions" do
|
115
134
|
proc{~(:x + 1 > 100)}.should_not raise_error
|
116
|
-
proc{~(:x + 1)}.should raise_error
|
135
|
+
proc{~(:x + 1)}.should raise_error
|
136
|
+
proc{~:x.sql_string}.should raise_error
|
137
|
+
proc{~:x.sql_number}.should raise_error
|
138
|
+
proc{~([:x, :y].sql_string_join)}.should raise_error
|
117
139
|
end
|
118
140
|
|
119
141
|
it "should not allow mathematical, inequality, or string operations on true, false, or nil" do
|
@@ -189,7 +211,7 @@ context "Blockless Ruby Filters" do
|
|
189
211
|
it "should support LiteralString" do
|
190
212
|
@d.l('x'.lit).should == '(x)'
|
191
213
|
@d.l(~'x'.lit).should == 'NOT x'
|
192
|
-
@d.l(~~'x'.lit).should == '
|
214
|
+
@d.l(~~'x'.lit).should == 'x'
|
193
215
|
@d.l(~(('x'.lit | :y) & :z)).should == '((NOT x AND NOT y) OR NOT z)'
|
194
216
|
@d.l(~(:x | 'y'.lit)).should == '(NOT x AND NOT y)'
|
195
217
|
@d.l(~('x'.lit & 'y'.lit)).should == '(NOT x OR NOT y)'
|
@@ -253,17 +275,23 @@ context "Blockless Ruby Filters" do
|
|
253
275
|
end
|
254
276
|
|
255
277
|
it "should support Array#sql_string_join for concatenation of SQL strings" do
|
256
|
-
@d.
|
257
|
-
@d.
|
258
|
-
@d.
|
259
|
-
@d.
|
260
|
-
@d.
|
261
|
-
@d.
|
262
|
-
@d.
|
263
|
-
@d.
|
264
|
-
@d.
|
265
|
-
@d.
|
266
|
-
@d.
|
267
|
-
@d.
|
278
|
+
@d.lit([:x].sql_string_join).should == '(x)'
|
279
|
+
@d.lit([:x].sql_string_join(', ')).should == '(x)'
|
280
|
+
@d.lit([:x, :y].sql_string_join).should == '(x || y)'
|
281
|
+
@d.lit([:x, :y].sql_string_join(', ')).should == "(x || ', ' || y)"
|
282
|
+
@d.lit([:x[1], :y|1].sql_string_join).should == '(x(1) || y[1])'
|
283
|
+
@d.lit([:x[1], 'y.z'.lit].sql_string_join(', ')).should == "(x(1) || ', ' || y.z)"
|
284
|
+
@d.lit([:x, 1, :y].sql_string_join).should == "(x || '1' || y)"
|
285
|
+
@d.lit([:x, 1, :y].sql_string_join(', ')).should == "(x || ', ' || '1' || ', ' || y)"
|
286
|
+
@d.lit([:x, 1, :y].sql_string_join(:y__z)).should == "(x || y.z || '1' || y.z || y)"
|
287
|
+
@d.lit([:x, 1, :y].sql_string_join(1)).should == "(x || '1' || '1' || '1' || y)"
|
288
|
+
@d.lit([:x, :y].sql_string_join('y.x || x.y'.lit)).should == "(x || y.x || x.y || y)"
|
289
|
+
@d.lit([[:x, :y].sql_string_join, [:a, :b].sql_string_join].sql_string_join).should == "((x || y) || (a || b))"
|
290
|
+
end
|
291
|
+
|
292
|
+
it "should support StringExpression#+ for concatenation of SQL strings" do
|
293
|
+
@d.lit(:x.sql_string + :y).should == '(x || y)'
|
294
|
+
@d.lit([:x].sql_string_join + :y).should == '((x) || y)'
|
295
|
+
@d.lit([:x, :z].sql_string_join(' ') + :y).should == "((x || ' ' || z) || y)"
|
268
296
|
end
|
269
297
|
end
|
data/spec/core_sql_spec.rb
CHANGED
@@ -213,7 +213,7 @@ end
|
|
213
213
|
|
214
214
|
context "Symbol#to_column_ref" do
|
215
215
|
setup do
|
216
|
-
@ds =
|
216
|
+
@ds = MockDataset.new(nil)
|
217
217
|
end
|
218
218
|
|
219
219
|
specify "should convert qualified symbol notation into dot notation" do
|
@@ -305,6 +305,29 @@ context "String#to_datetime" do
|
|
305
305
|
end
|
306
306
|
end
|
307
307
|
|
308
|
+
context "String#to_sequel_time" do
|
309
|
+
after do
|
310
|
+
Sequel.time_class = Time
|
311
|
+
end
|
312
|
+
|
313
|
+
specify "should convert the string into a Time object by default" do
|
314
|
+
"2007-07-11 10:11:12a".to_sequel_time.class.should == Time
|
315
|
+
"2007-07-11 10:11:12a".to_sequel_time.should == Time.parse("2007-07-11 10:11:12a")
|
316
|
+
end
|
317
|
+
|
318
|
+
specify "should convert the string into a DateTime object if that is set" do
|
319
|
+
Sequel.time_class = DateTime
|
320
|
+
"2007-07-11 10:11:12a".to_sequel_time.class.should == DateTime
|
321
|
+
"2007-07-11 10:11:12a".to_sequel_time.should == DateTime.parse("2007-07-11 10:11:12a")
|
322
|
+
end
|
323
|
+
|
324
|
+
specify "should raise Error::InvalidValue for an invalid time" do
|
325
|
+
proc {'0000-00-00'.to_sequel_time}.should raise_error(Sequel::Error::InvalidValue)
|
326
|
+
Sequel.time_class = DateTime
|
327
|
+
proc {'0000-00-00'.to_sequel_time}.should raise_error(Sequel::Error::InvalidValue)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
308
331
|
context "Sequel::SQL::Function#==" do
|
309
332
|
specify "should be true for functions with the same name and arguments, false otherwise" do
|
310
333
|
a = :date[:t]
|
data/spec/dataset_spec.rb
CHANGED
@@ -1183,7 +1183,7 @@ end
|
|
1183
1183
|
|
1184
1184
|
context "Dataset#join_table" do
|
1185
1185
|
setup do
|
1186
|
-
@d =
|
1186
|
+
@d = MockDataset.new(nil).from(:items)
|
1187
1187
|
@d.quote_identifiers = true
|
1188
1188
|
end
|
1189
1189
|
|
@@ -1269,7 +1269,7 @@ context "Dataset#join_table" do
|
|
1269
1269
|
@d.from('stats').join('players', {:id => :player_id}, 'p').sql.should ==
|
1270
1270
|
'SELECT * FROM "stats" INNER JOIN "players" "p" ON ("p"."id" = "stats"."player_id")'
|
1271
1271
|
|
1272
|
-
ds =
|
1272
|
+
ds = MockDataset.new(nil).from(:foo => :f)
|
1273
1273
|
ds.quote_identifiers = true
|
1274
1274
|
ds.join_table(:inner, :bar, :id => :bar_id).sql.should ==
|
1275
1275
|
'SELECT * FROM "foo" "f" INNER JOIN "bar" ON ("bar"."id" = "f"."bar_id")'
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequel_core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-06-
|
12
|
+
date: 2008-06-04 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|