Skip to content

Commit

Permalink
Add Dataset#select_prepend for prepending to the current selected col…
Browse files Browse the repository at this point in the history
…umns

In Ruby 1.8, hashes weren't ordered, so this method would not have made
sense. However, since Ruby 1.9, hashes are insertion ordered, and
it isn't uncommon to rely upon the order.  Sequel orders hashes by their
order in the returned values from the database, which should be the
same as the order of columns in the SELECT clause. Sequel has had a
dataset method for appending to the SELECT list basically forever,
this adds support for prepending to the SELECT list.

There are quite a few databases that support `SELECT *, column` but
not `SELECT column, *`, so when using select_prepend, if there isn't
an existing selection, Sequel always uses qualified table selections.
If anyone really cares, we can add a separate method so that
databases that support `SELECT column, *` can use it. Note that
using select_prepend after select_append, where the select_append
used `SELECT *, column`, may break on databases that do not support
selected columns before a bare wildcard.
  • Loading branch information
jeremyevans committed Mar 13, 2024
1 parent e393663 commit b703aae
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
=== master

* Add Dataset#select_prepend for prepending to the current selected columns (jeremyevans) (#2139)

=== 5.78.0 (2024-03-01)

* Support SQLite 3.45+ jsonb functions in the sqlite_json_ops extension (jeremyevans) (#2133)
Expand Down
4 changes: 3 additions & 1 deletion README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,12 @@ Like +order+, +select+ overrides an existing selection:
posts.select(:stamp).select(:name)
# SELECT name FROM posts

As you might expect, there is an +order_append+ equivalent for +select+ called +select_append+:
As you might expect, there are +order_append+ and +order_prepend+ equivalents for +select+ called +select_append+ and +select_prepend+:

posts.select(:stamp).select_append(:name)
# SELECT stamp, name FROM posts
posts.select(:stamp).select_prepend(:name)
# SELECT name, stamp FROM posts

=== Deleting Records

Expand Down
2 changes: 1 addition & 1 deletion doc/dataset_basics.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Most Dataset methods that users will use can be broken down into two types:

Most dataset methods fall into this category, which can be further broken down by the clause they affect:

SELECT:: select, select_all, select_append, select_group, select_more
SELECT:: select, select_all, select_append, select_group, select_more, select_prepend
FROM:: from, from_self
JOIN:: join, left_join, right_join, full_join, natural_join, natural_left_join, natural_right_join, natural_full_join, cross_join, inner_join, left_outer_join, right_outer_join, full_outer_join, join_table
WHERE:: where, filter, exclude, or, grep, invert, unfiltered
Expand Down
7 changes: 6 additions & 1 deletion doc/querying.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -624,11 +624,16 @@ Like +order+, +select+ replaces the existing selected columns:
Artist.select(:id).select(:name)
# SELECT name FROM artists

To add to the existing selected columns, use +select_append+:
To append to the existing selected columns, use +select_append+:

Artist.select(:id).select_append(:name)
# SELECT id, name FROM artists

To prepend to the existing selected columns, use +select_prepend+:

Artist.select(:id).select_prepend(:name)
# SELECT name, id FROM artists

To remove specifically selected columns, and default back to all
columns, use +select_all+:

Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/dataset/dataset_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class DatasetModule < ::Module
where exclude exclude_having having
distinct grep group group_and_count group_append
limit offset order order_append order_prepend reverse
select select_all select_append select_group server
select select_all select_append select_group select_prepend server
METHS

# Define a method in the module
Expand Down
40 changes: 31 additions & 9 deletions lib/sequel/dataset/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Dataset
add_graph_aliases distinct except exclude exclude_having
filter for_update from from_self graph grep group group_and_count group_append group_by having intersect invert
limit lock_style naked offset or order order_append order_by order_more order_prepend qualify
reverse reverse_order select select_all select_append select_group select_more server
reverse reverse_order select select_all select_append select_group select_more select_prepend server
set_graph_aliases unfiltered ungraphed ungrouped union
unlimited unordered where with with_recursive with_sql
METHS
Expand Down Expand Up @@ -944,14 +944,8 @@ def select_all(*tables)
# DB[:items].select(:a).select_append(:b) # SELECT a, b FROM items
# DB[:items].select_append(:b) # SELECT *, b FROM items
def select_append(*columns, &block)
cur_sel = @opts[:select]
if !cur_sel || cur_sel.empty?
unless supports_select_all_and_column?
return select_all(*(Array(@opts[:from]) + Array(@opts[:join]))).select_append(*columns, &block)
end
cur_sel = [WILDCARD]
end
select(*(cur_sel + columns), &block)
virtual_row_columns(columns, block)
select(*(_current_select(true) + columns))
end

# Set both the select and group clauses with the given +columns+.
Expand All @@ -973,6 +967,18 @@ def select_more(*columns, &block)
select_append(*columns, &block)
end

# Returns a copy of the dataset with the given columns added
# to the existing selected columns. If no columns are currently selected,
# it will select the columns given in addition to *.
#
# DB[:items].select(:a).select(:b) # SELECT b FROM items
# DB[:items].select(:a).select_prepend(:b) # SELECT b, a FROM items
# DB[:items].select_prepend(:b) # SELECT b, * FROM items
def select_prepend(*columns, &block)
virtual_row_columns(columns, block)
select(*(columns + _current_select(false)))
end

# Set the server for this dataset to use. Used to pick a specific database
# shard to run a query against, or to override the default (where SELECT uses
# :read_only database and all other queries use the :default database). This
Expand Down Expand Up @@ -1353,6 +1359,22 @@ def _extension!(exts)
end
# :nocov:

# A frozen array for the currently selected columns.
def _current_select(allow_plain_wildcard)
cur_sel = @opts[:select]

if !cur_sel || cur_sel.empty?
cur_sel = if allow_plain_wildcard && supports_select_all_and_column?
[WILDCARD].freeze
else
tables = Array(@opts[:from]) + Array(@opts[:join])
tables.map{|t| i, a = split_alias(t); a || i}.map!{|t| SQL::ColumnAll.new(t)}.freeze
end
end

cur_sel
end

# If invert is true, invert the condition.
def _invert_filter(cond, invert)
if invert
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/model/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Model
# natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by,
# order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join,
# right_outer_join, select, select_all, select_append, select_group, select_hash,
# select_hash_groups, select_map, select_more, select_order_map, server,
# select_hash_groups, select_map, select_more, select_order_map, select_prepend, server,
# single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups,
# truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all,
# where_each, where_single_value, with, with_recursive, with_sql
Expand Down
37 changes: 36 additions & 1 deletion spec/core/dataset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1472,7 +1472,7 @@ def supports_cte_in_subselect?; false end
@d.select(:blah).select_all.select_append(:a, :b).sql.must_equal 'SELECT *, a, b FROM test'
end

it "should add to the currently selected columns" do
it "should append to the currently selected columns" do
@d.select(:a).select_append(:b).sql.must_equal 'SELECT a, b FROM test'
@d.select(Sequel::SQL::ColumnAll.new(:a)).select_append(Sequel::SQL::ColumnAll.new(:b)).sql.must_equal 'SELECT a.*, b.* FROM test'
end
Expand All @@ -1491,6 +1491,35 @@ def supports_cte_in_subselect?; false end
end
end

describe "Dataset#select_prepend" do
before do
@d = Sequel.mock.dataset.from(:test)
end

it "should select * in addition to columns if no columns selected" do
@d.select_prepend(:a, :b).sql.must_equal 'SELECT a, b, test.* FROM test'
@d.select_all.select_prepend(:a, :b).sql.must_equal 'SELECT a, b, test.* FROM test'
@d.select(:blah).select_all.select_prepend(:a, :b).sql.must_equal 'SELECT a, b, test.* FROM test'
end

it "should prepend to the currently selected columns" do
@d.select(:a).select_prepend(:b).sql.must_equal 'SELECT b, a FROM test'
@d.select(Sequel::SQL::ColumnAll.new(:a)).select_prepend(Sequel::SQL::ColumnAll.new(:b)).sql.must_equal 'SELECT b.*, a.* FROM test'
end

it "should accept a block that yields a virtual row" do
@d.select(:a).select_prepend{|o| o.b}.sql.must_equal 'SELECT b, a FROM test'
@d.select(Sequel::SQL::ColumnAll.new(:a)).select_prepend(Sequel::SQL::ColumnAll.new(:b)){b(1)}.sql.must_equal 'SELECT b.*, b(1), a.* FROM test'
end

it "should select from all from and join tables" do
@d.select_prepend(:b).sql.must_equal 'SELECT b, test.* FROM test'
@d.from(:test, :c).select_prepend(:b).sql.must_equal 'SELECT b, test.*, c.* FROM test, c'
@d.cross_join(:c).select_prepend(:b).sql.must_equal 'SELECT b, test.*, c.* FROM test CROSS JOIN c'
@d.cross_join(:c).cross_join(:d).select_prepend(:b).sql.must_equal 'SELECT b, test.*, c.*, d.* FROM test CROSS JOIN c CROSS JOIN d'
end
end

describe "Dataset#order" do
before do
@dataset = Sequel.mock.dataset.from(:test)
Expand Down Expand Up @@ -1954,6 +1983,12 @@ def supports_cte_in_subselect?; false end
@ds.where(:bar).foo.sql.must_equal 'SELECT baz FROM items WHERE bar GROUP BY baz'
end

it "should have dataset_module support a select_prepend method" do
@ds = @ds.with_extend{select_prepend :foo, :baz}
@ds.foo.sql.must_equal 'SELECT baz, items.* FROM items'
@ds.where(:bar).foo.sql.must_equal 'SELECT baz, items.* FROM items WHERE bar'
end

it "should have dataset_module support a server method" do
@ds = @ds.with_extend{server :foo, :baz}
@ds.foo.opts[:server].must_equal :baz
Expand Down
4 changes: 4 additions & 0 deletions spec/integration/dataset_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@
@ds.all.must_equal [{:id=>1, :number=>10}]
end

it "should support select_prepend and select_append" do
@ds.select_prepend{number.as(:n)}.select_append{id.as(:i)}.first.to_a.must_equal [[:n, 10], [:id, 1], [:number, 10], [:i, 1]]
end

it "should skip locked rows correctly" do
skip if async? # async doesn't work with transactions
@ds.insert(:number=>10)
Expand Down
1 change: 1 addition & 0 deletions spec/model/class_dataset_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
@db.sqls.must_equal ["SELECT id FROM items"]
@c.select_order_map(:id).must_equal [1]
@db.sqls.must_equal ["SELECT id FROM items ORDER BY id"]
@c.select_prepend(:a).sql.must_equal "SELECT a, items.* FROM items"
@c.server(:a).opts[:server].must_equal :a
@c.single_record.must_equal @c.load(:id=>1)
@db.sqls.must_equal ["SELECT * FROM items LIMIT 1"]
Expand Down

0 comments on commit b703aae

Please sign in to comment.