Skip to content

Commit 6bea9e8

Browse files
authored
Add support for GROUP BY (CUBE, ROLLUP, SETS) (#18)
* Add additional features based on Snowflake support * Update odbc library version, Ruby versions * Update readme
1 parent a28ddf2 commit 6bea9e8

File tree

8 files changed

+181
-28
lines changed

8 files changed

+181
-28
lines changed

.github/workflows/gem-push.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v2
17-
- name: Set up Ruby 3.3
17+
- name: Set up Ruby 3.4
1818
uses: ruby/setup-ruby@v1
1919
with:
20-
ruby-version: 3.3
20+
ruby-version: 3.4
2121

2222
- name: Publish to RubyGems
2323
run: |

.github/workflows/ruby.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
ruby-version: ['3.2', '3.3']
22+
ruby-version: ['3.3', '3.4']
2323

2424
steps:
2525
- uses: actions/checkout@v2
@@ -30,7 +30,7 @@ jobs:
3030
- name: Install Snowflake ODBC driver
3131
run: curl ${SNOWFLAKE_DRIVER_URL} -o snowflake_driver.deb && sudo dpkg -i snowflake_driver.deb
3232
env:
33-
SNOWFLAKE_DRIVER_URL: https://sfc-repo.snowflakecomputing.com/odbc/linux/3.4.1/snowflake-odbc-3.4.1.x86_64.deb
33+
SNOWFLAKE_DRIVER_URL: https://sfc-repo.snowflakecomputing.com/odbc/linux/3.12.0/snowflake-odbc-3.12.0.x86_64.deb
3434
- name: Set up Ruby
3535
uses: ruby/setup-ruby@v1
3636
with:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## 2.3.0 / 2025-11-17
8+
* Add support for `GROUP CUBE`
9+
* Add support for `GROUP ROLLUP`
10+
* Add support for `GROUPING SETS`
11+
712
## 2.2.0 / 2023-10-17
813
* Add support for `MERGE` (credit: @benalavi)
914
* Add requirement for `sequel` v5.58.0 or newer (to support the new MERGE methods).

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ be taken down either via the `after(:each)` blocks or when the connection is clo
6262

6363
We have two workflows included in this project:
6464

65-
* Ruby (`ruby.yml`): This runs the specs for this gem against Ruby 3.0 and 3.1. Note
65+
* Ruby (`ruby.yml`): This runs the specs for this gem against Ruby 3.3 and 3.4. Note
6666
that this requires the secret `SNOWFLAKE_CONN_STR` to be set (see above for example connection string),
6767
as we need to connect to Snowflake to run tests. These specs will be run for every pull request,
6868
and is run after every commit to those branches.

lib/sequel-snowflake/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Sequel
22
module Snowflake
33
# sequel-snowflake version
4-
VERSION = "2.2.0"
4+
VERSION = "2.3.0"
55
end
66
end

lib/sequel/adapters/snowflake.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,22 @@ def fetch_rows(sql)
3838
self
3939
end
4040

41-
# Whether the MERGE statement is supported:
42-
# https://github.com/jeremyevans/sequel/blob/master/lib/sequel/dataset/features.rb#L129
43-
# Snowflake reference: https://docs.snowflake.com/en/sql-reference/sql/merge
41+
# https://docs.snowflake.com/en/sql-reference/constructs/group-by-cube
42+
def supports_group_cube?
43+
true
44+
end
45+
46+
# https://docs.snowflake.com/en/sql-reference/constructs/group-by-rollup
47+
def supports_group_rollup?
48+
true
49+
end
50+
51+
# https://docs.snowflake.com/en/sql-reference/constructs/group-by-grouping-sets
52+
def supports_grouping_sets?
53+
true
54+
end
55+
56+
# https://docs.snowflake.com/en/sql-reference/sql/merge
4457
def supports_merge?
4558
true
4659
end

mise.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tools]
2+
ruby = "latest"

spec/sequel/adapters/snowflake_spec.rb

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
require 'securerandom'
22

33
describe Sequel::Snowflake::Dataset do
4-
let(:db) { Sequel.connect(adapter: :snowflake, drvconnect: ENV['SNOWFLAKE_CONN_STR']) }
4+
let(:db) { @db ||= Sequel.connect(adapter: :snowflake, drvconnect: ENV['SNOWFLAKE_CONN_STR']) }
5+
6+
before(:all) do
7+
@db = Sequel.connect(adapter: :snowflake, drvconnect: ENV['SNOWFLAKE_CONN_STR'])
8+
end
9+
10+
after(:all) do
11+
@db.disconnect if @db
12+
end
513

614
describe 'Converting Snowflake data types' do
715
# Create a test table with a reasonably-random suffix
@@ -70,33 +78,157 @@
7078
end
7179
end
7280

81+
describe 'GROUP BY features' do
82+
before(:all) do
83+
@products = "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym
84+
@sales = "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym
85+
86+
@db.create_table(@products, :temp => true) do
87+
Integer :product_id
88+
Float :wholesale_price
89+
end
90+
91+
@db.create_table(@sales, :temp => true) do
92+
Integer :product_id
93+
Float :retail_price
94+
Integer :quantity
95+
String :city
96+
String :state
97+
end
98+
99+
@db[@products].insert({ product_id: 1, wholesale_price: 1.00 })
100+
@db[@products].insert({ product_id: 2, wholesale_price: 2.00 })
101+
@db[@sales].insert({ product_id: 1, retail_price: 2.00, quantity: 1, city: 'SF', state: 'CA' })
102+
@db[@sales].insert({ product_id: 1, retail_price: 2.00, quantity: 2, city: 'SJ', state: 'CA' })
103+
@db[@sales].insert({ product_id: 2, retail_price: 5.00, quantity: 4, city: 'SF', state: 'CA' })
104+
@db[@sales].insert({ product_id: 2, retail_price: 5.00, quantity: 8, city: 'SJ', state: 'CA' })
105+
@db[@sales].insert({ product_id: 2, retail_price: 5.00, quantity: 16, city: 'Miami', state: 'FL' })
106+
@db[@sales].insert({ product_id: 2, retail_price: 5.00, quantity: 32, city: 'Orlando', state: 'FL' })
107+
@db[@sales].insert({ product_id: 2, retail_price: 5.00, quantity: 64, city: 'SJ', state: 'CA' })
108+
end
109+
110+
after(:all) do
111+
@db.drop_table(@products) if @products
112+
@db.drop_table(@sales) if @sales
113+
end
114+
115+
let(:products) { @products }
116+
let(:sales) { @sales }
117+
118+
it 'can use GROUP CUBE' do
119+
res = db.from(Sequel[products].as(:p)).
120+
join(Sequel[sales].as(:s), Sequel[:p][:product_id] => Sequel[:s][:product_id]).
121+
select(
122+
Sequel[:s][:state],
123+
Sequel[:s][:city],
124+
Sequel.function(:sum, Sequel.*(Sequel.-(Sequel[:s][:retail_price], Sequel[:p][:wholesale_price]), Sequel[:s][:quantity])).as(:profit)
125+
).
126+
group(Sequel[:s][:state], Sequel[:s][:city]).
127+
group_cube.
128+
order(Sequel.asc(Sequel[:s][:state], nulls: :last)).
129+
order_append(Sequel[:s][:city]).
130+
all
131+
132+
expect(res).to match_array([
133+
{ state: 'CA', city: 'SF', profit: 13 },
134+
{ state: 'CA', city: 'SJ', profit: 218 },
135+
{ state: 'CA', city: nil, profit: 231 },
136+
{ state: 'FL', city: 'Miami', profit: 48 },
137+
{ state: 'FL', city: 'Orlando', profit: 96 },
138+
{ state: 'FL', city: nil, profit: 144 },
139+
{ state: nil, city: 'Miami', profit: 48 },
140+
{ state: nil, city: 'Orlando', profit: 96 },
141+
{ state: nil, city: 'SF', profit: 13 },
142+
{ state: nil, city: 'SJ', profit: 218 },
143+
{ state: nil, city: nil, profit: 375 },
144+
])
145+
end
146+
147+
it 'can use GROUP ROLLUP' do
148+
res = db.from(Sequel[products].as(:p)).
149+
join(Sequel[sales].as(:s), Sequel[:p][:product_id] => Sequel[:s][:product_id]).
150+
select(
151+
Sequel[:s][:state],
152+
Sequel[:s][:city],
153+
Sequel.function(:sum, Sequel.*(Sequel.-(Sequel[:s][:retail_price], Sequel[:p][:wholesale_price]), Sequel[:s][:quantity])).as(:profit)
154+
).
155+
group(Sequel[:s][:state], Sequel[:s][:city]).
156+
group_rollup.
157+
order(Sequel.asc(Sequel[:s][:state], nulls: :last)).
158+
order_append(Sequel[:s][:city]).
159+
all
160+
161+
expect(res).to match_array([
162+
{ state: 'CA', city: 'SF', profit: 13 },
163+
{ state: 'CA', city: 'SJ', profit: 218 },
164+
{ state: 'CA', city: nil, profit: 231 },
165+
{ state: 'FL', city: 'Miami', profit: 48 },
166+
{ state: 'FL', city: 'Orlando', profit: 96 },
167+
{ state: 'FL', city: nil, profit: 144 },
168+
{ state: nil, city: nil, profit: 375 },
169+
])
170+
end
171+
172+
it 'can use GROUPING SETS' do
173+
res = db.from(Sequel[products].as(:p)).
174+
join(Sequel[sales].as(:s), Sequel[:p][:product_id] => Sequel[:s][:product_id]).
175+
select(
176+
Sequel[:s][:state],
177+
Sequel[:s][:city],
178+
Sequel.function(:sum, Sequel.*(Sequel.-(Sequel[:s][:retail_price], Sequel[:p][:wholesale_price]), Sequel[:s][:quantity])).as(:profit)
179+
).
180+
group([Sequel[:s][:state]], [Sequel[:s][:city]]).
181+
grouping_sets.
182+
order(Sequel.asc(Sequel[:s][:state], nulls: :last)).
183+
order_append(Sequel[:s][:city]).
184+
all
185+
186+
expect(res).to match_array([
187+
{ state: 'CA', city: nil, profit: 231 },
188+
{ state: 'FL', city: nil, profit: 144 },
189+
{ state: nil, city: 'Miami', profit: 48 },
190+
{ state: nil, city: 'Orlando', profit: 96 },
191+
{ state: nil, city: 'SF', profit: 13 },
192+
{ state: nil, city: 'SJ', profit: 218 },
193+
])
194+
end
195+
end
196+
73197
describe 'MERGE feature' do
74-
let(:target_table) { "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym }
75-
let(:source_table) { "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym }
198+
before(:all) do
199+
@target_table = "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym
200+
@source_table = "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym
76201

77-
before(:each) do
78-
db.create_table(target_table, :temp => true) do
202+
@db.create_table(@target_table, :temp => true) do
79203
String :str
80204
String :str2
81205
String :str3
82206
end
83207

84-
db.create_table(source_table, :temp => true) do
208+
@db.create_table(@source_table, :temp => true) do
85209
String :from
86210
String :to
87211
String :whomst
88212
end
213+
end
89214

90-
db[target_table].insert({ str: 'foo', str2: 'foo', str3: 'phoo' })
91-
db[target_table].insert({ str: 'baz', str2: 'foo', str3: 'buzz' })
92-
db[source_table].insert({ from: 'foo', to: 'bar', whomst: 'me' })
215+
after(:all) do
216+
@db.drop_table(@target_table) if @target_table
217+
@db.drop_table(@source_table) if @source_table
93218
end
94219

95-
after(:each) do
96-
db.drop_table(target_table)
97-
db.drop_table(source_table)
220+
before(:each) do
221+
# Clear and repopulate data for each test since MERGE modifies data
222+
db[@target_table].delete
223+
db[@source_table].delete
224+
db[@target_table].insert({ str: 'foo', str2: 'foo', str3: 'phoo' })
225+
db[@target_table].insert({ str: 'baz', str2: 'foo', str3: 'buzz' })
226+
db[@source_table].insert({ from: 'foo', to: 'bar', whomst: 'me' })
98227
end
99228

229+
let(:target_table) { @target_table }
230+
let(:source_table) { @source_table }
231+
100232
it 'can use MERGE' do
101233
db[target_table].merge_using(source_table, str: :from).merge_update(str2: :to).merge
102234

@@ -108,26 +240,27 @@
108240
end
109241

110242
describe '#explain' do
111-
# Create a test table with a reasonably-random suffix
112-
let(:test_table) { "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym }
243+
before(:all) do
244+
@test_table = "SEQUEL_SNOWFLAKE_SPECS_#{SecureRandom.hex(10)}".to_sym
113245

114-
before(:each) do
115-
db.create_table(test_table, :temp => true) do
246+
@db.create_table(@test_table, :temp => true) do
116247
Numeric :id
117248
String :name
118249
String :email
119250
String :title
120251
end
121252

122-
db[test_table].insert(
253+
@db[@test_table].insert(
123254
{ id: 1, name: 'John Null', email: '[email protected]', title: 'Software Tester' }
124255
)
125256
end
126257

127-
after(:each) do
128-
db.drop_table(test_table)
258+
after(:all) do
259+
@db.drop_table(@test_table) if @test_table
129260
end
130261

262+
let(:test_table) { @test_table }
263+
131264
it "should have explain output" do
132265
query = db.fetch("SELECT * FROM #{test_table} WHERE ID=1;")
133266

0 commit comments

Comments
 (0)