Permalink
Browse files

Add Sequel.delay for generic delayed evaluation

The Sequel dataset layer has lacked a generic delayed evaluation
utility before this.  For dates/timestamps, it's recommended to
use Sequel::CURRENT_DATE and Sequel::CURRENT_TIMESTAMP instead of
a delayed Date.today or Time.now, and most other places where
delayed evaluation could be useful (e.g. model associations),
Sequel already handles by creating a new dataset every time.

Still, I think a generic delayed evaluation facility is useful
to have, and since it is fairly simple to implement and I may
want to use it internally in the future, I decided to add it to
the core library.
  • Loading branch information...
1 parent 7b0f0eb commit 9beafa5ef53a613e95f9ccd2d4bceb27e05ac47d @jeremyevans committed Oct 29, 2012
Showing with 100 additions and 0 deletions.
  1. +4 −0 CHANGELOG
  2. +29 −0 doc/object_model.rdoc
  3. +7 −0 lib/sequel/dataset/sql.rb
  4. +36 −0 lib/sequel/sql.rb
  5. +24 −0 spec/core/expression_filters_spec.rb
View
@@ -1,7 +1,11 @@
=== HEAD
+* Add Sequel.delay for generic delayed evaluation (jeremyevans)
+
* Make uniqueness validations correctly handle nil values (jeremyevans)
+* Support :unlogged option for create_table on PostgreSQL (JonathanTron) (#575)
+
* Add ConnectionPool#pool_type to get the type of connection pool in use (jeremyevans)
* Explicitly mark primary keys as NOT NULL on SQLite (jeremyevans)
View
@@ -395,6 +395,35 @@ These objects are usually used as values in queries:
DB[:table].insert(:time=>Sequel::CURRENT_TIMESTAMP)
+=== Sequel::SQL::DelayedEvaluation
+
+Sequel::SQL::DelayedEvaluation objects represent an evaluation that is delayed
+until query literalization.
+
+ Sequel::SQL::DelayedEvaluation.new(proc{some_model.updated_at})
+
+The following shortcut exists for creating Sequel::SQL::DelayedEvaluation
+objects:
+
+ Sequel.delay{some_model.updated_at}
+
+Note how Sequel.delay requires a block, while Sequel::SQL::DelayedEvaluation.new
+accepts a generic callable object.
+
+Let's say you wanted a dataset for the number of objects greater than some
+attribute of another object:
+
+ ds = DB[:table].where{updated_at > some_model.updated_at}
+
+The problem with the above query is that it evaluates "some_model.updated_at"
+statically, so if you change some_model.updated_at later, it won't affect this
+dataset. You can use Sequel.delay to fix this:
+
+ ds = DB[:table].where{updated_at > Sequel.delay{some_model.updated_at}}
+
+This will evaluate "some_model.updated_at" every time you literalize the
+dataset (usually every time it is executed).
+
=== Sequel::SQL::Function
Sequel::SQL::Function objects represents database function calls, which take a function
@@ -290,6 +290,7 @@ def self.clause_methods(type, clauses)
column_all_sql
complex_expression_sql
constant_sql
+ delayed_evaluation_sql
function_sql
join_clause_sql
join_on_clause_sql
@@ -495,6 +496,12 @@ def constant_sql_append(sql, constant)
sql << constant.to_s
end
+ # SQL fragment for delayed evaluations, evaluating the
+ # object and literalizing the returned value.
+ def delayed_evaluation_sql_append(sql, callable)
+ literal_append(sql, callable.call)
+ end
+
# SQL fragment specifying an emulated SQL function call.
# By default, assumes just the function name may need to
# be emulated, adapters should set an EMULATED_FUNCTION_MAP
View
@@ -377,6 +377,27 @@ def char_length(arg)
SQL::EmulatedFunction.new(:char_length, arg)
end
+ # Return a delayed evaluation that uses the passed block. This is used
+ # to delay evaluations of the code to runtime. For example, with
+ # the following code:
+ #
+ # ds = DB[:table].where{column > Time.now}
+ #
+ # The filter is fixed to the time that where was called. Unless you are
+ # only using the dataset once immediately after creating it, that's
+ # probably not desired. If you just want to set it to the time when the
+ # query is sent to the database, you can wrap it in Sequel.delay:
+ #
+ # ds = DB[:table].where{column > Sequel.delay{Time.now}}
+ #
+ # Note that for dates and timestamps, you are probably better off using
+ # Sequel::CURRENT_DATE and Sequel::CURRENT_TIMESTAMP instead of this
+ # generic delayed evaluation facility.
+ def delay(&block)
+ raise(Error, "Sequel.delay requires a block") unless block
+ SQL::DelayedEvaluation.new(block)
+ end
+
# Order the given argument descending.
# Options:
#
@@ -1140,6 +1161,21 @@ class ComplexExpression
Constants::NULL=>Constants::NOTNULL, Constants::NOTNULL=>Constants::NULL}
end
+ # Represents a delayed evaluation, encapsulating a callable
+ # object which returns the value to use when called.
+ class DelayedEvaluation < GenericExpression
+ # A callable object that returns the value of the evaluation
+ # when called.
+ attr_reader :callable
+
+ # Set the callable object
+ def initialize(callable)
+ @callable = callable
+ end
+
+ to_s_method :delayed_evaluation_sql, '@callable'
+ end
+
# Represents an SQL function call.
class Function < GenericExpression
# The SQL function to call
@@ -966,3 +966,27 @@ def o.sql_literal(ds) 'foo' end
Sequel.recursive_map([[nil]], proc{|s| s.to_i}).should == [[nil]]
end
end
+
+describe "Sequel.delay" do
+ specify "should delay calling the block until literalization" do
+ o = Class.new do
+ def a
+ @a ||= 0
+ @a += 1
+ end
+ def _a
+ @a
+ end
+ end.new
+ ds = Sequel.mock[:b].where(:a=>Sequel.delay{o.a})
+ o._a.should be_nil
+ ds.sql.should == "SELECT * FROM b WHERE (a = 1)"
+ o._a.should == 1
+ ds.sql.should == "SELECT * FROM b WHERE (a = 2)"
+ o._a.should == 2
+ end
+
+ specify "should raise if called without a block" do
+ proc{Sequel.delay}.should raise_error(Sequel::Error)
+ end
+end

0 comments on commit 9beafa5

Please sign in to comment.