Skip to content

Commit 1b44870

Browse files
committed
Add standard Ruby collection methods to Result and Row
- Add Result#empty? for checking empty result sets - Add Row#dig for safe nested value access (useful for VARIANT/JSON columns) - Add Row#to_h for explicit hash conversion - Add Row#key?/has_key? for column existence checks - Add Row#fetch for access with default values or error handling - Add unit tests (28 test cases, no Snowflake required) Addresses missing methods discovered during BigQuery to Snowflake migration.
1 parent 588c4af commit 1b44870

File tree

6 files changed

+159
-1
lines changed

6 files changed

+159
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9+
### Added
10+
- `Result#empty?` - Check if result set has no rows
11+
- `Row#dig` - Safe nested value access for VARIANT/JSON columns
12+
- `Row#to_h` - Explicit hash conversion (was implicit via Enumerable)
13+
- `Row#key?` / `Row#has_key?` - Check if column exists
14+
- `Row#fetch` - Access column with default value or error handling
915

1016
## [1.5.0] - 2025-10-14
1117
### Added

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
rb_snowflake_client (1.4.0)
4+
rb_snowflake_client (1.5.0)
55
bigdecimal (>= 3.0)
66
concurrent-ruby (>= 1.2)
77
connection_pool (>= 2.4)

lib/ruby_snowflake/result.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def size
3939

4040
alias length size
4141

42+
def empty?
43+
size.zero?
44+
end
45+
4246
def first
4347
wrap_row(data.first.first)
4448
end

lib/ruby_snowflake/row.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,30 @@ def values
7777
map { |_, v| v }
7878
end
7979

80+
def to_h
81+
@column_to_index.each_with_object({}) { |(name, _), hash| hash[name] = self[name] }
82+
end
83+
84+
def dig(key, *rest)
85+
value = self[key]
86+
return value if rest.empty? || value.nil?
87+
return nil unless value.respond_to?(:dig)
88+
value.dig(*rest)
89+
end
90+
91+
def key?(key)
92+
@column_to_index.key?(key.to_s.downcase)
93+
end
94+
alias has_key? key?
95+
96+
def fetch(key, *args, &block)
97+
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1..2)" if args.size > 1
98+
return self[key] if key?(key)
99+
return args.first if args.any?
100+
return yield(key) if block
101+
raise KeyError, "key not found: #{key.inspect}"
102+
end
103+
80104
def to_s
81105
to_h.to_s
82106
end

spec/ruby_snowflake/result_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require "spec_helper"
2+
3+
RSpec.describe RubySnowflake::Result do
4+
describe "#empty?" do
5+
context "when result has no rows" do
6+
it "returns true" do
7+
row_types = [{"name" => "id", "type" => "fixed", "scale" => 0, "precision" => 0}]
8+
result = RubySnowflake::Result.new(1, row_types)
9+
result[0] = []
10+
11+
expect(result.empty?).to be true
12+
end
13+
end
14+
15+
context "when result has rows" do
16+
it "returns false" do
17+
row_types = [{"name" => "id", "type" => "fixed", "scale" => 0, "precision" => 0}]
18+
result = RubySnowflake::Result.new(1, row_types)
19+
result[0] = [["1"]]
20+
21+
expect(result.empty?).to be false
22+
end
23+
end
24+
25+
context "when result has multiple partitions but all empty" do
26+
it "returns true" do
27+
row_types = [{"name" => "id", "type" => "fixed", "scale" => 0, "precision" => 0}]
28+
result = RubySnowflake::Result.new(2, row_types)
29+
result[0] = []
30+
result[1] = []
31+
32+
expect(result.empty?).to be true
33+
end
34+
end
35+
end
36+
end

spec/ruby_snowflake/row_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require "spec_helper"
2+
3+
RSpec.describe RubySnowflake::Row do
4+
let(:row_types) do
5+
[
6+
{type: :text, scale: 0, precision: 0, name: :name},
7+
{type: :fixed, scale: 0, precision: 0, name: :age}
8+
]
9+
end
10+
let(:column_to_index) { {"name" => 0, "age" => 1} }
11+
let(:data) { ["Alice", "30"] }
12+
let(:row) { described_class.new(row_types, column_to_index, data) }
13+
14+
describe "#to_h" do
15+
it "converts row to hash with string keys and type conversion" do
16+
result = row.to_h
17+
expect(result).to eq({"name" => "Alice", "age" => 30})
18+
expect(result["age"]).to be_a(Integer)
19+
end
20+
end
21+
22+
describe "#dig" do
23+
it "accesses values case-insensitively for strings and symbols" do
24+
expect(row.dig("name")).to eq("Alice")
25+
expect(row.dig(:name)).to eq("Alice")
26+
expect(row.dig("NAME")).to eq("Alice")
27+
end
28+
29+
it "returns nil for missing key" do
30+
expect(row.dig("missing")).to be_nil
31+
end
32+
33+
it "digs into nested hashes" do
34+
nested_row_types = [{type: :text, scale: 0, precision: 0, name: :data}]
35+
nested_column_to_index = {"data" => 0}
36+
nested_data = [{"user" => {"name" => "Bob", "age" => 25}}]
37+
nested_row = described_class.new(nested_row_types, nested_column_to_index, nested_data)
38+
39+
expect(nested_row.dig("data", "user", "name")).to eq("Bob")
40+
expect(nested_row.dig("data", "user", "age")).to eq(25)
41+
expect(nested_row.dig("data", "missing", "key")).to be_nil
42+
end
43+
44+
it "digs into nested arrays" do
45+
array_row_types = [{type: :text, scale: 0, precision: 0, name: :items}]
46+
array_column_to_index = {"items" => 0}
47+
array_data = [[{"id" => 1}, {"id" => 2}]]
48+
array_row = described_class.new(array_row_types, array_column_to_index, array_data)
49+
50+
expect(array_row.dig("items", 0, "id")).to eq(1)
51+
expect(array_row.dig("items", 1, "id")).to eq(2)
52+
end
53+
end
54+
55+
describe "#key?" do
56+
it "checks column existence case-insensitively" do
57+
expect(row.key?("name")).to be true
58+
expect(row.key?(:NAME)).to be true
59+
expect(row.key?("missing")).to be false
60+
end
61+
end
62+
63+
describe "#fetch" do
64+
it "returns value for existing key" do
65+
expect(row.fetch("name")).to eq("Alice")
66+
expect(row.fetch(:AGE)).to eq(30)
67+
end
68+
69+
it "returns default value for missing key" do
70+
expect(row.fetch("missing", "default")).to eq("default")
71+
expect(row.fetch(:nonexistent, 0)).to eq(0)
72+
end
73+
74+
it "yields to block for missing key" do
75+
expect(row.fetch("missing") { |k| "no #{k}" }).to eq("no missing")
76+
expect(row.fetch(:missing) { |k| "key: #{k}" }).to eq("key: missing")
77+
end
78+
79+
it "raises KeyError for missing key without default or block" do
80+
expect { row.fetch("missing") }.to raise_error(KeyError, /key not found/)
81+
expect { row.fetch(:nonexistent) }.to raise_error(KeyError, /key not found/)
82+
end
83+
84+
it "raises ArgumentError for too many arguments" do
85+
expect { row.fetch("name", "default1", "default2") }.to raise_error(ArgumentError, /wrong number of arguments/)
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)