Skip to content

Commit a0e5da9

Browse files
committed
Add Dataset#as_set and Dataset#select_set, as Set will be a core class in Ruby 4
1 parent 3d75b09 commit a0e5da9

File tree

7 files changed

+210
-7
lines changed

7 files changed

+210
-7
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
=== master
22

3+
* Add Dataset#as_set and Dataset#select_set, as Set will be a core class in Ruby 4 (jeremyevans)
4+
35
* Add support for Database#create_index :only option on PostgreSQL 11+ (KirIgor) (#2343)
46

57
* Add support for Database#indexes :invalid option on PostgresSQL (KirIgor, jeremyevans) (#2343)

lib/sequel/core.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen-string-literal: true
22

3-
%w'bigdecimal date thread time uri'.each{|f| require f}
3+
%w'bigdecimal date set thread time uri'.each{|f| require f}
44

55
# Top level module for Sequel
66
#

lib/sequel/dataset/actions.rb

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ class Dataset
1111

1212
# Action methods defined by Sequel that execute code on the database.
1313
ACTION_METHODS = (<<-METHS).split.map(&:to_sym).freeze
14-
<< [] all as_hash avg count columns columns! delete each
14+
<< [] all as_hash as_set avg count columns columns! delete each
1515
empty? fetch_rows first first! get import insert last
16-
map max min multi_insert paged_each select_hash select_hash_groups select_map select_order_map
17-
single_record single_record! single_value single_value! sum to_hash to_hash_groups truncate update
18-
where_all where_each where_single_value
16+
map max min multi_insert paged_each select_hash select_hash_groups
17+
select_map select_order_map select_set single_record single_record!
18+
single_value single_value! sum to_hash to_hash_groups
19+
truncate update where_all where_each where_single_value
1920
METHS
2021

2122
# The clone options to use when retrieving columns for a dataset.
@@ -51,6 +52,26 @@ def all(&block)
5152
_all(block){|a| each{|r| a << r}}
5253
end
5354

55+
# Returns sets for column values for each record in the dataset.
56+
#
57+
# DB[:table].as_set(:id) # SELECT * FROM table
58+
# # => Set[1, 2, 3, ...]
59+
#
60+
# You can also provide an array of column names, in which case the elements
61+
# of the returned set are arrays (not sets):
62+
#
63+
# DB[:table].as_set([:id, :name]) # SELECT * FROM table
64+
# # => Set[[1, 'A'], [2, 'B'], [3, 'C'], ...]
65+
def as_set(column)
66+
return naked.as_set(column) if row_proc
67+
68+
if column.is_a?(Array)
69+
to_set{|r| r.values_at(*column)}
70+
else
71+
to_set{|r| r[column]}
72+
end
73+
end
74+
5475
# Returns the average value for the given column/expression.
5576
# Uses a virtual row block if no argument is given.
5677
#
@@ -765,6 +786,35 @@ def select_order_map(column=nil, &block)
765786
_select_map(column, true, &block)
766787
end
767788

789+
# Selects the column given (either as an argument or as a block), and
790+
# returns a set of all values of that column in the dataset.
791+
#
792+
# DB[:table].select_set(:id) # SELECT id FROM table
793+
# # => Set[3, 5, 8, 1, ...]
794+
#
795+
# DB[:table].select_set{id * 2} # SELECT (id * 2) FROM table
796+
# # => Set[6, 10, 16, 2, ...]
797+
#
798+
# You can also provide an array of column names, which returns a set
799+
# with array elements (not set elements):
800+
#
801+
# DB[:table].select_map([:id, :name]) # SELECT id, name FROM table
802+
# # => Set[[1, 'A'], [2, 'B'], [3, 'C'], ...]
803+
#
804+
# If you provide an array of expressions, you must be sure that each entry
805+
# in the array has an alias that Sequel can determine.
806+
def select_set(column=nil, &block)
807+
ds = ungraphed.naked
808+
columns = Array(column)
809+
virtual_row_columns(columns, block)
810+
select_cols = order ? columns.map{|c| c.is_a?(SQL::OrderedExpression) ? c.expression : c} : columns
811+
if column.is_a?(Array) || (columns.length > 1)
812+
ds.select(*select_cols)._select_set_multiple(hash_key_symbols(select_cols))
813+
else
814+
ds.select(auto_alias_expression(select_cols.first))._select_set_single
815+
end
816+
end
817+
768818
# Limits the dataset to one record, and returns the first record in the dataset,
769819
# or nil if the dataset has no records. Users should probably use +first+ instead of
770820
# this method. Example:
@@ -1089,6 +1139,17 @@ def _select_map_single
10891139
map{|r| r[k||=r.keys.first]}
10901140
end
10911141

1142+
# Return a set of arrays of values given by the symbols in ret_cols.
1143+
def _select_set_multiple(ret_cols)
1144+
to_set{|r| r.values_at(*ret_cols)}
1145+
end
1146+
1147+
# Returns a set of the first value in each row.
1148+
def _select_set_single
1149+
k = nil
1150+
to_set{|r| r[k||=r.keys.first]}
1151+
end
1152+
10921153
# A dataset for returning single values from the current dataset.
10931154
def single_value_ds
10941155
clone(:limit=>1).ungraphed.naked

lib/sequel/model/base.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Model
1010
# Class methods for Sequel::Model that implement basic model functionality.
1111
#
1212
# * All of the following methods have class methods created that send the method
13-
# to the model's dataset: all, any?, as_hash, avg, count, cross_join, distinct, each,
13+
# to the model's dataset: all, any?, as_hash, as_set, avg, count, cross_join, distinct, each,
1414
# each_server, empty?, except, exclude, exclude_having, fetch_rows,
1515
# filter, first, first!, for_update, from, from_self, full_join, full_outer_join,
1616
# get, graph, grep, group, group_and_count, group_append, group_by, having, import,
@@ -19,7 +19,7 @@ class Model
1919
# natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by,
2020
# order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join,
2121
# right_outer_join, select, select_all, select_append, select_group, select_hash,
22-
# select_hash_groups, select_map, select_more, select_order_map, select_prepend, server,
22+
# select_hash_groups, select_map, select_more, select_order_map, select_prepend, select_set, server,
2323
# single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups,
2424
# truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all,
2525
# where_each, where_single_value, with, with_recursive, with_sql

spec/core/dataset_spec.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2314,6 +2314,26 @@ def ==(o) @h == o.h; end
23142314
end
23152315
end
23162316

2317+
describe "Dataset#as_set" do
2318+
before do
2319+
@d = Sequel.mock(:fetch=>[{:a => 1, :b => 2}, {:a => 3, :b => 4}, {:a => 5, :b => 6}])[:items]
2320+
end
2321+
2322+
it "should return set of column elements if column symbol is given" do
2323+
@d.as_set(:a).must_equal Set[1, 3, 5]
2324+
end
2325+
2326+
it "should support set with array elements if an array of column symbols is given" do
2327+
@d.as_set([:a, :b]).must_equal Set[[1, 2], [3, 4], [5, 6]]
2328+
end
2329+
2330+
it "should not call the row_proc" do
2331+
@d = @d.with_row_proc(proc{|r| h = {}; r.keys.each{|k| h[k] = r[k] * 2}; h})
2332+
@d.as_set(:a).must_equal Set[1, 3, 5]
2333+
@d.as_set([:a, :b]).must_equal Set[[1, 2], [3, 4], [5, 6]]
2334+
end
2335+
end
2336+
23172337
describe "Dataset#distinct" do
23182338
before do
23192339
@db = Sequel.mock
@@ -5420,6 +5440,100 @@ def default_timestamp_format
54205440
end
54215441
end
54225442

5443+
describe "Sequel::Dataset#select_set" do
5444+
before do
5445+
@ds = Sequel.mock(:fetch=>[{:c=>1}, {:c=>2}])[:t]
5446+
end
5447+
5448+
it "should do select and map in one step" do
5449+
@ds.select_set(:a).must_equal Set[1, 2]
5450+
@ds.db.sqls.must_equal ['SELECT a FROM t']
5451+
end
5452+
5453+
it "should handle qualified identifiers in arguments" do
5454+
@ds.select_set(Sequel[:a][:b]).must_equal Set[1, 2]
5455+
@ds.db.sqls.must_equal ['SELECT a.b FROM t']
5456+
end
5457+
5458+
with_symbol_splitting "should handle implicit qualifiers in arguments" do
5459+
@ds.select_set(:a__b).must_equal Set[1, 2]
5460+
@ds.db.sqls.must_equal ['SELECT a.b FROM t']
5461+
end
5462+
5463+
it "should raise if multiple arguments and can't determine alias" do
5464+
proc{@ds.select_set([Sequel.function(:a), :b])}.must_raise(Sequel::Error)
5465+
proc{@ds.select_set(Sequel.function(:a)){b}}.must_raise(Sequel::Error)
5466+
proc{@ds.select_set{[a.function, b]}}.must_raise(Sequel::Error)
5467+
end
5468+
5469+
with_symbol_splitting "should handle implicit aliases in arguments" do
5470+
@ds.select_set(:a___b).must_equal Set[1, 2]
5471+
@ds.db.sqls.must_equal ['SELECT a AS b FROM t']
5472+
end
5473+
5474+
it "should handle aliased expressions in arguments" do
5475+
@ds.select_set(Sequel[:a].as(:b)).must_equal Set[1, 2]
5476+
@ds.db.sqls.must_equal ['SELECT a AS b FROM t']
5477+
end
5478+
5479+
it "should handle other objects" do
5480+
@ds.select_set(Sequel.lit("a").as(:b)).must_equal Set[1, 2]
5481+
@ds.db.sqls.must_equal ['SELECT a AS b FROM t']
5482+
end
5483+
5484+
it "should handle identifiers with strings" do
5485+
@ds.select_set([Sequel::SQL::Identifier.new('c'), :c]).must_equal Set[[1, 1], [2, 2]]
5486+
@ds.db.sqls.must_equal ['SELECT c, c FROM t']
5487+
end
5488+
5489+
it "should raise an error for plain strings" do
5490+
proc{@ds.select_set(['c', :c])}.must_raise(Sequel::Error)
5491+
@ds.db.sqls.must_equal []
5492+
end
5493+
5494+
it "should handle an expression without a determinable alias" do
5495+
@ds.select_set{a(t[c])}.must_equal Set[1, 2]
5496+
@ds.db.sqls.must_equal ['SELECT a(t.c) AS v FROM t']
5497+
end
5498+
5499+
it "should accept a block" do
5500+
@ds.select_set{a(t[c]).as(b)}.must_equal Set[1, 2]
5501+
@ds.db.sqls.must_equal ['SELECT a(t.c) AS b FROM t']
5502+
end
5503+
5504+
it "should accept a block with an array of columns" do
5505+
@ds.select_set{[a(t[c]).as(c), a(t[c]).as(c)]}.must_equal Set[[1, 1], [2, 2]]
5506+
@ds.db.sqls.must_equal ['SELECT a(t.c) AS c, a(t.c) AS c FROM t']
5507+
end
5508+
5509+
it "should accept a block with a column" do
5510+
@ds.select_set(:c){a(t[c]).as(c)}.must_equal Set[[1, 1], [2, 2]]
5511+
@ds.db.sqls.must_equal ['SELECT c, a(t.c) AS c FROM t']
5512+
end
5513+
5514+
it "should accept a block and array of arguments" do
5515+
@ds.select_set([:c, :c]){[a(t[c]).as(c), a(t[c]).as(c)]}.must_equal Set[[1, 1, 1, 1], [2, 2, 2, 2]]
5516+
@ds.db.sqls.must_equal ['SELECT c, c, a(t.c) AS c, a(t.c) AS c FROM t']
5517+
end
5518+
5519+
it "should handle an array of columns" do
5520+
@ds.select_set([:c, :c]).must_equal Set[[1, 1], [2, 2]]
5521+
@ds.db.sqls.must_equal ['SELECT c, c FROM t']
5522+
@ds.select_set([Sequel.expr(:d).as(:c), Sequel.qualify(:b, :c), Sequel.identifier(:c), Sequel.identifier(:c).qualify(:b)]).must_equal Set[[1, 1, 1, 1], [2, 2, 2, 2]]
5523+
@ds.db.sqls.must_equal ['SELECT d AS c, b.c, c, b.c FROM t']
5524+
end
5525+
5526+
with_symbol_splitting "should handle an array of columns with splittable symbols" do
5527+
@ds.select_set([:a__c, :a__d___c]).must_equal Set[[1, 1], [2, 2]]
5528+
@ds.db.sqls.must_equal ['SELECT a.c, a.d AS c FROM t']
5529+
end
5530+
5531+
it "should handle an array with a single element" do
5532+
@ds.select_set([:c]).must_equal Set[[1], [2]]
5533+
@ds.db.sqls.must_equal ['SELECT c FROM t']
5534+
end
5535+
end
5536+
54235537
describe "Dataset#lock_style and for_update" do
54245538
before do
54255539
@ds = Sequel.mock.dataset.from(:t)

spec/integration/dataset_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,12 @@
14871487
ds.to_hash_groups(:a, :d, :hash => (tmp = {})).must_be_same_as(tmp)
14881488
end
14891489

1490+
it "should have working #as_set" do
1491+
@ds.as_set(:a).must_equal Set[1, 5]
1492+
@ds.as_set(:b).must_equal Set[2, 6]
1493+
@ds.as_set([:a, :b]).must_equal Set[[1, 2], [5, 6]]
1494+
end
1495+
14901496
it "should have working #select_map" do
14911497
@ds.select_map(:a).must_equal [1, 5]
14921498
@ds.select_map(:b).must_equal [2, 6]
@@ -1548,6 +1554,22 @@
15481554
ds.select_hash_groups(:a, :d, :hash => (tmp = {})).must_be_same_as(tmp)
15491555
@ds.extension(:null_dataset).nullify.select_hash_groups(:a, :d).must_equal({})
15501556
end
1557+
1558+
it "should have working #select_set" do
1559+
@ds.select_set(:a).must_equal Set[1, 5]
1560+
@ds.select_set(:b).must_equal Set[2, 6]
1561+
@ds.select_set([:a]).must_equal Set[[1], [5]]
1562+
@ds.select_set([:a, :b]).must_equal Set[[1, 2], [5, 6]]
1563+
@ds.extension(:null_dataset).nullify.select_set([:a, :b]).must_equal Set[]
1564+
1565+
@ds.select_set(Sequel[:a].as(:e)).must_equal Set[1, 5]
1566+
@ds.select_set(Sequel[:b].as(:e)).must_equal Set[2, 6]
1567+
@ds.select_set([Sequel[:a].as(:e), Sequel[:b].as(:f)]).must_equal Set[[1, 2], [5, 6]]
1568+
@ds.select_set([Sequel[:a][:a].as(:e), Sequel[:a][:b].as(:f)]).must_equal Set[[1, 2], [5, 6]]
1569+
@ds.select_set([Sequel.expr(Sequel[:a][:a]).as(:e), Sequel.expr(Sequel[:a][:b]).as(:f)]).must_equal Set[[1, 2], [5, 6]]
1570+
@ds.select_set([Sequel.qualify(:a, :a).as(:e), Sequel.qualify(:a, :b).as(:f)]).must_equal Set[[1, 2], [5, 6]]
1571+
@ds.select_set([Sequel.identifier(:a).qualify(:a).as(:e), Sequel.qualify(:a, :b).as(:f)]).must_equal Set[[1, 2], [5, 6]]
1572+
end
15511573
end
15521574

15531575
describe "Sequel::Dataset DSL support" do

spec/model/class_dataset_methods_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
@db.sqls.must_equal ["SELECT * FROM items"]
1414
@c.any?.must_equal true
1515
@db.sqls.must_equal ["SELECT * FROM items"]
16+
@c.as_set(:id).must_equal Set[1]
17+
@db.sqls.must_equal ["SELECT * FROM items"]
1618
@c.avg(:id).must_equal 1
1719
@db.sqls.must_equal ["SELECT avg(id) AS avg FROM items LIMIT 1"]
1820
@c.count.must_equal 1
@@ -100,6 +102,8 @@
100102
@c.select_order_map(:id).must_equal [1]
101103
@db.sqls.must_equal ["SELECT id FROM items ORDER BY id"]
102104
@c.select_prepend(:a).sql.must_equal "SELECT a, items.* FROM items"
105+
@c.select_set(:id).must_equal Set[1]
106+
@db.sqls.must_equal ["SELECT id FROM items"]
103107
@c.server(:a).opts[:server].must_equal :a
104108
@c.single_record.must_equal @c.load(:id=>1)
105109
@db.sqls.must_equal ["SELECT * FROM items LIMIT 1"]

0 commit comments

Comments
 (0)