Permalink
Browse files

Add Dataset #select_map, #select_order_map, and #select_hash

select_map and select_order_map both return arrays of values for the
column specified.  The column can be specified either via an argument
or a block, similar to Dataset#get.  Both accept any valid objects as
arguments.

select_hash returns a hash.  It requires two symbol arguments, but
can handle implicit qualifiers or aliases in the symbols.

Neither of these methods offer any new functionality, they just cut
down on the number of required key strokes:

  select_map(:column)       # select(:column).map(:column)
  select_order_map(:column) # select(:column).order(:column).
                            #  map(:column)
  select_hash(:key_column, :value_column)
  # select(:key_column, :value_column).
  #  to_hash(:key_column, :value_column)

This makes it easier to get simple ruby arrays and hashes out of
datasets.

To implement select_hash, add private method Dataset#hash_key_symbol,
which attempts to guess the hash key symbol that will be used for
the given column symbol.

While here, remove Dataset::GET_ERROR_MSG and Dataset::MAP_ERROR_MSG
constants, replacing both with Dataset::ARG_BLOCK_ERROR_MSG.
  • Loading branch information...
1 parent 917dc3c commit ee3445294f5a83fdc02d5f03129eac839fdc74d2 @jeremyevans committed Nov 20, 2009
Showing with 158 additions and 5 deletions.
  1. +2 −0 CHANGELOG
  2. +39 −5 lib/sequel/dataset/convenience.rb
  3. +117 −0 spec/core/dataset_spec.rb
View
@@ -1,5 +1,7 @@
=== HEAD
+* Add Dataset #select_map, #select_order_map, and #select_hash (jeremyevans)
+
* Make Dataset#group_and_count handle arguments other than Symbols (jeremyevans)
* Add :only_if_modified option to validates_unique method in validation_helpers plugin (jeremyevans)
@@ -3,8 +3,7 @@ class Dataset
COMMA_SEPARATOR = ', '.freeze
COUNT_OF_ALL_AS_COUNT = SQL::Function.new(:count, LiteralString.new('*'.freeze)).as(:count)
ARRAY_ACCESS_ERROR_MSG = 'You cannot call Dataset#[] with an integer or with no arguments.'.freeze
- MAP_ERROR_MSG = 'Using Dataset#map with an argument and a block is not allowed'.freeze
- GET_ERROR_MSG = 'must provide argument or block to Dataset#get, not both'.freeze
+ ARG_BLOCK_ERROR_MSG = 'Must use either an argument or a block, not both'.freeze
IMPORT_ERROR_MSG = 'Using Sequel::Dataset#import an empty column array is not allowed'.freeze
# Returns the first record matching the conditions. Examples:
@@ -74,7 +73,7 @@ def first(*args, &block)
# ds.get{|o| o.sum(:id)}
def get(column=nil, &block)
if column
- raise(Error, GET_ERROR_MSG) if block
+ raise(Error, ARG_BLOCK_ERROR_MSG) if block
select(column).single_value
else
select(&block).single_value
@@ -154,7 +153,7 @@ def last(*args, &block)
# ds.map{|r| r[:id] * 2} => [2, 4, 6, ...]
def map(column=nil, &block)
if column
- raise(Error, MAP_ERROR_MSG) if block
+ raise(Error, ARG_BLOCK_ERROR_MSG) if block
super(){|r| r[column]}
else
super(&block)
@@ -195,6 +194,32 @@ def range(column)
end
end
+ def select_hash(key_column, value_column)
+ select(key_column, value_column).to_hash(hash_key_symbol(key_column), hash_key_symbol(value_column))
+ end
+
+ def select_map(column=nil, &block)
+ ds = naked.ungraphed
+ ds = if column
+ raise(Error, ARG_BLOCK_ERROR_MSG) if block
+ ds.select(column)
+ else
+ ds.select(&block)
+ end
+ ds.map{|r| r.values.first}
+ end
+
+ def select_order_map(column=nil, &block)
+ ds = naked.ungraphed
+ ds = if column
+ raise(Error, ARG_BLOCK_ERROR_MSG) if block
+ ds.select(column).order(unaliased_identifier(column))
+ else
+ ds.select(&block).order(&block)
+ end
+ ds.map{|r| r.values.first}
+ end
+
# Returns the first record in the dataset.
def single_record
clone(:limit=>1).each{|r| return r}
@@ -204,7 +229,7 @@ def single_record
# Returns the first value of the first record in the dataset.
# Returns nil if dataset is empty.
def single_value
- if r = naked.clone(:graph=>false).single_record
+ if r = naked.ungraphed.single_record
r.values.first
end
end
@@ -244,6 +269,15 @@ def to_hash(key_column, value_column = nil)
private
+ # Return a plain symbol given a potentially qualified or aliased symbol,
+ # specifying the symbol that is likely to be used as the hash key
+ # for the column when records are returned.
+ def hash_key_symbol(s)
+ raise(Error, "#{s.inspect} is not a symbol") unless s.is_a?(Symbol)
+ _, c, a = split_symbol(s)
+ (a || c).to_sym
+ end
+
# Return the unaliased part of the identifier. Handles both
# implicit aliases in symbols, as well as SQL::AliasedExpression
# objects. Other objects are returned as is.
@@ -3413,3 +3413,120 @@ def @ds.fetch_rows(sql, &block)
Sequel.typecast_timezone.should == :utc
end
end
+
+context "Sequel::Dataset#select_map" do
+ before do
+ @ds = MockDatabase.new[:t]
+ def @ds.fetch_rows(sql)
+ db << sql
+ yield({:c=>1})
+ yield({:c=>2})
+ end
+ @ds.db.reset
+ end
+
+ specify "should do select and map in one step" do
+ @ds.select_map(:a).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a FROM t']
+ end
+
+ specify "should handle implicit qualifiers in arguments" do
+ @ds.select_map(:a__b).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a.b FROM t']
+ end
+
+ specify "should handle implicit aliases in arguments" do
+ @ds.select_map(:a___b).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a AS b FROM t']
+ end
+
+ specify "should handle other objects" do
+ @ds.select_map("a".lit.as(:b)).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a AS b FROM t']
+ end
+
+ specify "should accept a block" do
+ @ds.select_map{a(t__c)}.should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a(t.c) FROM t']
+ end
+end
+
+context "Sequel::Dataset#select_order_map" do
+ before do
+ @ds = MockDatabase.new[:t]
+ def @ds.fetch_rows(sql)
+ db << sql
+ yield({:c=>1})
+ yield({:c=>2})
+ end
+ @ds.db.reset
+ end
+
+ specify "should do select and map in one step" do
+ @ds.select_order_map(:a).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a FROM t ORDER BY a']
+ end
+
+ specify "should handle implicit qualifiers in arguments" do
+ @ds.select_order_map(:a__b).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a.b FROM t ORDER BY a.b']
+ end
+
+ specify "should handle implicit aliases in arguments" do
+ @ds.select_order_map(:a___b).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a AS b FROM t ORDER BY a']
+ end
+
+ specify "should handle implicit qualifiers and aliases in arguments" do
+ @ds.select_order_map(:t__a___b).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT t.a AS b FROM t ORDER BY t.a']
+ end
+
+ specify "should handle AliasedExpressions" do
+ @ds.select_order_map("a".lit.as(:b)).should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a AS b FROM t ORDER BY a']
+ end
+
+ specify "should accept a block" do
+ @ds.select_order_map{a(t__c)}.should == [1, 2]
+ @ds.db.sqls.should == ['SELECT a(t.c) FROM t ORDER BY a(t.c)']
+ end
+end
+
+context "Sequel::Dataset#select_hash" do
+ before do
+ @ds = MockDatabase.new[:t]
+ def @ds.set_fr_yield(hs)
+ @hs = hs
+ end
+ def @ds.fetch_rows(sql)
+ db << sql
+ @hs.each{|h| yield h}
+ end
+ @ds.db.reset
+ end
+
+ specify "should do select and map in one step" do
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
+ @ds.select_hash(:a, :b).should == {1=>2, 3=>4}
+ @ds.db.sqls.should == ['SELECT a, b FROM t']
+ end
+
+ specify "should handle implicit qualifiers in arguments" do
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
+ @ds.select_hash(:t__a, :t__b).should == {1=>2, 3=>4}
+ @ds.db.sqls.should == ['SELECT t.a, t.b FROM t']
+ end
+
+ specify "should handle implicit aliases in arguments" do
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
+ @ds.select_hash(:c___a, :d___b).should == {1=>2, 3=>4}
+ @ds.db.sqls.should == ['SELECT c AS a, d AS b FROM t']
+ end
+
+ specify "should handle implicit qualifiers and aliases in arguments" do
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
+ @ds.select_hash(:t__c___a, :t__d___b).should == {1=>2, 3=>4}
+ @ds.db.sqls.should == ['SELECT t.c AS a, t.d AS b FROM t']
+ end
+end

0 comments on commit ee34452

Please sign in to comment.