From e1e3207583c39ec69ea030e29b331868308c672c Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 15 Mar 2012 12:11:34 -0700 Subject: [PATCH] Add null_dataset extension, for creating a dataset that never issues 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. --- CHANGELOG | 2 + lib/sequel/extensions/null_dataset.rb | 90 +++++++++++++++++++++++++++ spec/extensions/null_dataset_spec.rb | 85 +++++++++++++++++++++++++ spec/extensions/spec_helper.rb | 2 +- www/pages/plugins | 1 + 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 lib/sequel/extensions/null_dataset.rb create mode 100644 spec/extensions/null_dataset_spec.rb diff --git a/CHANGELOG b/CHANGELOG index be9da928a4..a7e034958b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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) diff --git a/lib/sequel/extensions/null_dataset.rb b/lib/sequel/extensions/null_dataset.rb new file mode 100644 index 0000000000..dd5d680107 --- /dev/null +++ b/lib/sequel/extensions/null_dataset.rb @@ -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 diff --git a/spec/extensions/null_dataset_spec.rb b/spec/extensions/null_dataset_spec.rb new file mode 100644 index 0000000000..7ebcc9909d --- /dev/null +++ b/spec/extensions/null_dataset_spec.rb @@ -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 diff --git a/spec/extensions/spec_helper.rb b/spec/extensions/spec_helper.rb index da88f8b5c4..660c8843d4 100644 --- a/spec/extensions/spec_helper.rb +++ b/spec/extensions/spec_helper.rb @@ -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'] diff --git a/www/pages/plugins b/www/pages/plugins index 7392422db3..200c6339bd 100644 --- a/www/pages/plugins +++ b/www/pages/plugins @@ -85,6 +85,7 @@
  • looser_typecasting: Uses .to_f and .to_i instead of Kernel.Float and Kernel.Integer when typecasting floats and integers.
  • migration: Adds Migration and Migrator classes for easily migrating the database schema forward or reverting to a previous version.
  • named_timezones: Allows you to use named timezones instead of just :local and :utc (requires TZInfo).
  • +
  • null_dataset: Adds Dataset#nullify to get a dataset that will never issue a query.
  • pagination: Adds Dataset#paginate for easier pagination of datasets.
  • pretty_table: Adds Dataset#print for printing a dataset as a simple plain-text table.
  • query: Adds Dataset#query for a different interface to creating queries that doesn't use method chaining.