Skip to content

Commit

Permalink
Add null_dataset extension, for creating a dataset that never issues …
Browse files Browse the repository at this point in the history
…a database query

This is another feature that ActiveRecord will be adding in 4.0.
Basically, it's the null object pattern for datasets, so you can
create a dataset that never returns any rows (or issues other
queries).

This is easily implemented as an extension, where Dataset#nullify
just extends the cloned dataset with a module.

Note that there may be corner cases on some adapters when using
non-standard dataset methods.  Those can be dealt with on a
case by case basis.
  • Loading branch information
jeremyevans committed Mar 15, 2012
1 parent 9a7ae22 commit e1e3207
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== HEAD

* Add null_dataset extension, for creating a dataset that never issues a database query (jeremyevans)

* Database#uri and #url now return nil if a connection string was not used when connecting (jeremyevans) (#453)

* Add schema_caching extension, to speed up loading a large number of models by loading cached schema information from a file (jeremyevans)
Expand Down
90 changes: 90 additions & 0 deletions lib/sequel/extensions/null_dataset.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# The null_dataset extension adds the Dataset#nullify method, which
# returns a cloned dataset that will never issue a query to the
# database. It implements the null object pattern for datasets.
#
# The most common usage is probably in a method that must return
# a dataset, where the method knows the dataset shouldn't return
# anything. With standard Sequel, you'd probably just add a
# WHERE condition that is always false, but that still results
# in a query being sent to the database, and can be overridden
# using #unfiltered, the OR operator, or a UNION.
#
# Usage:
#
# ds = DB[;items].nullify.where(:a=>:b).select(:c)
# ds.sql # => "SELECT c FROM items WHERE (a = b)"
# ds.all # => [] # no query sent to the database
#
# Note that there is one case where a null dataset will sent
# a query to the database. If you call #columns on a nulled
# dataset and the dataset doesn't have an already cached
# version of the columns, it will create a new dataset with
# the same options to get the columns.

module Sequel
class Dataset
module NullDataset
# Create a new dataset from the dataset (which won't
# be nulled) to get the columns if they aren't already cached.
def columns
@columns ||= db.dataset(@opts).columns
end

# Return 0 without sending a database query.
def delete
0
end

# Return self without sending a database query, never yielding.
def each
self
end

# Return nil without sending a database query, never yielding.
def fetch_rows(sql)
nil
end

# Return nil without sending a database query.
def insert(*)
nil
end

# Return nil without sending a database query.
def truncate
nil
end

# Return 0 without sending a database query.
def update(v={})
0
end

protected

# Return nil without sending a database query.
def _import(columns, values, opts)
nil
end

private

# Just in case these are called directly by some internal code,
# make them noops. There's nothing we can do if the db
# is accessed directly to make a change, though.
(%w'_ddl _dui _insert' << '').each do |m|
class_eval("private; def execute#{m}(sql, opts={}) end", __FILE__, __LINE__)
end
end

# Return a cloned nullified dataset.
def nullify
clone.nullify!
end

# Nullify the current dataset
def nullify!
extend NullDataset
end
end
end
85 changes: 85 additions & 0 deletions spec/extensions/null_dataset_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")

describe "null_dataset extension" do
before do
@db = Sequel::mock(:fetch=>{:id=>1}, :autoid=>1, :numrows=>1, :columns=>[:id])
@ds = @db[:table].nullify
@i = 0
@pr = proc{|*a| @i += 1}
end
after do
@db.sqls.should == [] unless @skip_check
end

it "should make each be a noop" do
@ds.each(&@pr).should equal(@ds)
@i.should == 0
end

it "should make fetch_rows be a noop" do
@ds.fetch_rows("SELECT 1", &@pr).should == nil
@i.should == 0
end

it "should make insert be a noop" do
@ds.insert(1).should == nil
end

it "should make update be a noop" do
@ds.update(:a=>1).should == 0
end

it "should make delete be a noop" do
@ds.delete.should == 0
end

it "should make truncate be a noop" do
@ds.truncate.should == nil
end

it "should make execute_* be a noop" do
@ds.send(:execute_ddl,'FOO').should == nil
@ds.send(:execute_insert,'FOO').should == nil
@ds.send(:execute_dui,'FOO').should == nil
@ds.send(:execute,'FOO').should == nil
end

it "should have working columns" do
@skip_check = true
@ds.columns.should == [:id]
@db.sqls.should == ['SELECT * FROM table LIMIT 1']
end

it "should have count return 0" do
@ds.count.should == 0
end

it "should have empty return true" do
@ds.empty?.should == true
end

it "should make import a noop" do
@ds.import([:id], [[1], [2], [3]]).should == nil
end

it "should have nullify method returned modified receiver" do
@skip_check = true
ds = @db[:table]
ds.nullify.should_not equal(ds)
ds.each(&@pr)
@db.sqls.should == ['SELECT * FROM table']
@i.should == 1
end

it "should have null! method modify receiver" do
ds = @db[:table]
ds.nullify!.should equal(ds)
ds.each(&@pr)
@i.should == 0
end

it "should work with method chaining" do
@ds.where(:a=>1).select(:b).each(&@pr)
@i.should == 0
end
end
2 changes: 1 addition & 1 deletion spec/extensions/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
require 'sequel/model'
end

Sequel.extension(*%w'string_date_time inflector pagination query pretty_table blank migration schema_dumper looser_typecasting sql_expr thread_local_timezones to_dot columns_introspection server_block arbitrary_servers pg_auto_parameterize pg_statement_cache pg_hstore pg_hstore_ops schema_caching')
Sequel.extension(*%w'string_date_time inflector pagination query pretty_table blank migration schema_dumper looser_typecasting sql_expr thread_local_timezones to_dot columns_introspection server_block arbitrary_servers pg_auto_parameterize pg_statement_cache pg_hstore pg_hstore_ops schema_caching null_dataset')
{:hook_class_methods=>[], :schema=>[], :validation_class_methods=>[]}.each{|p, opts| Sequel::Model.plugin(p, *opts)}

Sequel::Dataset.introspect_all_columns if ENV['SEQUEL_COLUMNS_INTROSPECTION']
Expand Down
1 change: 1 addition & 0 deletions www/pages/plugins
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<li><a href="rdoc-plugins/files/lib/sequel/extensions/looser_typecasting_rb.html">looser_typecasting</a>: Uses .to_f and .to_i instead of Kernel.Float and Kernel.Integer when typecasting floats and integers.</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/migration_rb.html">migration</a>: Adds Migration and Migrator classes for easily migrating the database schema forward or reverting to a previous version.</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/named_timezones_rb.html">named_timezones</a>: Allows you to use named timezones instead of just :local and :utc (requires <a href="http://tzinfo.rubyforge.org/">TZInfo</a>).</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/null_dataset_rb.html">null_dataset</a>: Adds Dataset#nullify to get a dataset that will never issue a query.</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/pagination_rb.html">pagination</a>: Adds Dataset#paginate for easier pagination of datasets.</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/pretty_table_rb.html">pretty_table</a>: Adds Dataset#print for printing a dataset as a simple plain-text table.</li>
<li><a href="rdoc-plugins/files/lib/sequel/extensions/query_rb.html">query</a>: Adds Dataset#query for a different interface to creating queries that doesn't use method chaining.</li>
Expand Down

0 comments on commit e1e3207

Please sign in to comment.