sequel_core 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG 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