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 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.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 => :to_time, # MYSQL_TYPE_TIMESTAMP
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 => :to_time, # MYSQL_TYPE_DATETIME
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::ComplexExpression.new(:AND, filt, {:c__table_schema=>opts[:database] || self.opts[:database]}) if opts[:database] || !opts.include?(:database)
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 op == :'!~'}(#{literal(args.at(0))} REGEXP BINARY #{literal(args.at(1))})"
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.to_f },
172
- 1082 => lambda{ |s| s.to_date },
173
- 1083 => lambda{ |s| s.to_time },
174
- 1114 => lambda{ |s| s.to_time },
175
- 1184 => lambda{ |s| s.to_time },
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::ComplexExpression.new(:AND, filt, {:c__table_schema=>opts[:schema] || 'public'}) if opts[:schema] || !opts.include?(:schema)
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};"
@@ -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
@@ -2,31 +2,31 @@
2
2
  # code.
3
3
 
4
4
  class Array
5
- # Return a Sequel::SQL::ComplexExpression created from this array, not matching any of the
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::ComplexExpression created from this array, matching all of the
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::ComplexExpression created from this array, matching none
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::ComplexExpression created from this array, matching any of the
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::ComplexExpression representing an SQL string made up of the
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::ComplexExpression.new(:'||', *args)
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::ComplexExpression from this array.
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::ComplexExpression.from_value_pairs(self, *args)
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::ComplexExpression created from this hash, matching
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::ComplexExpression.new(:AND, self, ce)
68
+ ::Sequel::SQL::BooleanExpression.new(:AND, self, ce)
69
69
  end
70
70
 
71
- # Return a Sequel::SQL::ComplexExpression created from this hash, matching
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::ComplexExpression.new(:OR, self, ce)
75
+ ::Sequel::SQL::BooleanExpression.new(:OR, self, ce)
76
76
  end
77
77
 
78
- # Return a Sequel::SQL::ComplexExpression created from this hash, not matching any of the
78
+ # Return a Sequel::SQL::BooleanExpression created from this hash, not matching any of the
79
79
  # conditions.
80
80
  def ~
81
- ::Sequel::SQL::ComplexExpression.from_value_pairs(self, :OR, true)
81
+ ::Sequel::SQL::BooleanExpression.from_value_pairs(self, :OR, true)
82
82
  end
83
83
 
84
- # Return a Sequel::SQL::ComplexExpression created from this hash, matching all of the
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::ComplexExpression.from_value_pairs(self)
87
+ ::Sequel::SQL::BooleanExpression.from_value_pairs(self)
88
88
  end
89
89
 
90
- # Return a Sequel::SQL::ComplexExpression created from this hash, matching none
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::ComplexExpression.from_value_pairs(self, :AND, true)
93
+ ::Sequel::SQL::BooleanExpression.from_value_pairs(self, :AND, true)
94
94
  end
95
95
 
96
- # Return a Sequel::SQL::ComplexExpression created from this hash, matching any of the
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::ComplexExpression.from_value_pairs(self, :OR)
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::ComplexExpression using the *
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::ComplexExpression where this column (which should be boolean)
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})
@@ -396,18 +396,24 @@ module Sequel
396
396
  else
397
397
  raise ArgumentError, "invalid value for Date: #{value.inspect}"
398
398
  end
399
- when :datetime
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
- DateTime.new(value.year, value.month, value.day, value.hour, value.min, value.sec)
402
+ value
407
403
  when String
408
- value.to_datetime
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
- raise ArgumentError, "invalid value for DateTime: #{value.inspect}"
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::ComplexExpression === cond ? ~cond : SQL::ComplexExpression.new(:NOT, cond)
111
- cond = SQL::ComplexExpression.new(:AND, @opts[clause], cond) if @opts[clause]
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::ComplexExpression - an existing condition expression,
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::ComplexExpression.new(:AND, @opts[clause], cond) if @opts[clause] && !@opts[clause].blank?
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::ComplexExpression.like. Note that the
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::ComplexExpression.new(:OR, *Array(cols).collect{|c| SQL::ComplexExpression.like(c, *terms)}))
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
- o[:having] = SQL::ComplexExpression === having ? ~having : SQL::ComplexExpression.new(:NOT, having)
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::ComplexExpression.new(:OR, @opts[clause], filter_expr(block || cond)))
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. It
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::ComplexExpression.from_value_pairs(expr)
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::ComplexExpression.from_value_pairs(expr)
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(Sequel::Error, 'Invalid filter argument')
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 /\A(date)\z/
234
+ when /\Adate\z/
235
235
  :date
236
- when /\A(datetime|time|timestamp( with(out)? time zone)?)\z/
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/
@@ -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
- ### Classes ###
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
- # Most ruby operators methods are defined via metaprogramming: +, -, /, *, <, >, <=,
71
- # >=, & (AND), | (OR). This allows for a simple DSL after some core
72
- # classes have been overloaded with ComplexExpressionMethods.
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
- # Operator symbols that result in boolean SQL output
96
- BOOLEAN_RESULT_OPERATORS = BOOLEAN_OPERATORS + EQUALITY_OPERATORS + SEARCH_OPERATORS + INCLUSION_OPERATORS + [:NOT]
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 for method creation
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 = EQUALITY_OPERATORS + SEARCH_OPERATORS + INCLUSION_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 = MATHEMATICAL_OPERATORS + BOOLEAN_OPERATORS + STRING_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(Sequel::Error, 'mathematical and boolean operators require at least 1 argument') unless args.length >= 1
152
+ raise(Error, "The #{op} operator requires at least 1 argument") unless args.length >= 1
141
153
  when *TWO_ARITY_OPERATORS
142
- raise(Sequel::Error, '(in)equality operators require precisely 2 arguments') unless args.length == 2
143
- when :NOT
144
- raise(Sequel::Error, 'the NOT operator requires a single argument') unless args.length == 1
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(Sequel::Error, "invalid operator #{op}")
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 ? ~ce : ce
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.collect! do |ce|
200
- op, expr = Regexp === ce ? [ce.casefold? ? :'~*' : :~, ce.source] : [:LIKE, ce.to_s]
201
- new(op, l, expr)
202
- end
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.
@@ -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 case insensitive regular expressions with REGEXP" do
717
- @d.literal(:x.like(/a/i)).should == "(x REGEXP 'a')"
718
- @d.literal(~:x.like(/a/i)).should == "NOT (x REGEXP 'a')"
719
- end
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 == "NOT (x REGEXP BINARY 'a')"
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[:xyz].should == '000'
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, :xyz}.should raise_error(Sequel::Error)
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[:test2].delete
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.add_column :test2, :xyz, :float
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 ~ \'b\'))'
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 !~ \'b\'))'
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(Sequel::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 == '(x)'
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.l([:x].sql_string_join).should == '(x)'
257
- @d.l([:x].sql_string_join(', ')).should == '(x)'
258
- @d.l([:x, :y].sql_string_join).should == '(x || y)'
259
- @d.l([:x, :y].sql_string_join(', ')).should == "(x || ', ' || y)"
260
- @d.l([:x[1], :y|1].sql_string_join).should == '(x(1) || y[1])'
261
- @d.l([:x[1], 'y.z'.lit].sql_string_join(', ')).should == "(x(1) || ', ' || y.z)"
262
- @d.l([:x, 1, :y].sql_string_join).should == "(x || '1' || y)"
263
- @d.l([:x, 1, :y].sql_string_join(', ')).should == "(x || ', ' || '1' || ', ' || y)"
264
- @d.l([:x, 1, :y].sql_string_join(:y__z)).should == "(x || y.z || '1' || y.z || y)"
265
- @d.l([:x, 1, :y].sql_string_join(1)).should == "(x || '1' || '1' || '1' || y)"
266
- @d.l([:x, :y].sql_string_join('y.x || x.y'.lit)).should == "(x || y.x || x.y || y)"
267
- @d.l([[:x, :y].sql_string_join, [:a, :b].sql_string_join].sql_string_join).should == "((x || y) || (a || b))"
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
@@ -213,7 +213,7 @@ end
213
213
 
214
214
  context "Symbol#to_column_ref" do
215
215
  setup do
216
- @ds = Sequel::Dataset.new(nil)
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 = Sequel::Dataset.new(nil).from(:items)
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 = Sequel::Dataset.new(nil).from(:foo => :f)
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
@@ -20,6 +20,10 @@ class MockDataset < Sequel::Dataset
20
20
  @db.execute(sql)
21
21
  yield({:id => 1, :x => 1})
22
22
  end
23
+
24
+ def quoted_identifier(c)
25
+ "\"#{c}\""
26
+ end
23
27
  end
24
28
 
25
29
  class MockDatabase < Sequel::Database
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.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-01 00:00:00 -07:00
12
+ date: 2008-06-04 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15