Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

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

## [1.5.0] - 2025-10-14
### Added
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
rb_snowflake_client (1.4.0)
rb_snowflake_client (1.5.0)
bigdecimal (>= 3.0)
concurrent-ruby (>= 1.2)
connection_pool (>= 2.4)
Expand Down Expand Up @@ -65,7 +65,7 @@ GEM
securerandom (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.0.4)
uri (1.1.1)

PLATFORMS
arm64-darwin-22
Expand All @@ -81,4 +81,4 @@ DEPENDENCIES
rspec

BUNDLED WITH
2.5.10
4.0.3
4 changes: 4 additions & 0 deletions lib/ruby_snowflake/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def size

alias length size

def empty?
size.zero?
end

def first
wrap_row(data.first.first)
end
Expand Down
24 changes: 24 additions & 0 deletions lib/ruby_snowflake/row.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ def values
map { |_, v| v }
end

def to_h
@column_to_index.each_with_object({}) { |(name, _), hash| hash[name] = self[name] }
end

def dig(key, *rest)
value = self[key]
return value if rest.empty? || value.nil?
return nil unless value.respond_to?(:dig)
value.dig(*rest)
end

def key?(key)
@column_to_index.key?(key.to_s.downcase)
end
alias has_key? key?

def fetch(key, *args, &block)
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1..2)" if args.size > 1
return self[key] if key?(key)
return args.first if args.any?
return yield(key) if block
raise KeyError, "key not found: #{key.inspect}"
end

def to_s
to_h.to_s
end
Expand Down
53 changes: 53 additions & 0 deletions spec/ruby_snowflake/row_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require "spec_helper"

RSpec.describe RubySnowflake::Row do
describe "#dig" do
it "digs into nested hashes" do
nested_row_types = [{type: :text, scale: 0, precision: 0, name: :data}]
nested_column_to_index = {"data" => 0}
nested_data = [{"user" => {"name" => "Bob", "age" => 25}}]
nested_row = described_class.new(nested_row_types, nested_column_to_index, nested_data)

expect(nested_row.dig("data", "user", "name")).to eq("Bob")
expect(nested_row.dig("data", "user", "age")).to eq(25)
expect(nested_row.dig("data", "missing", "key")).to be_nil
end

it "digs into nested arrays" do
array_row_types = [{type: :text, scale: 0, precision: 0, name: :items}]
array_column_to_index = {"items" => 0}
array_data = [[{"id" => 1}, {"id" => 2}]]
array_row = described_class.new(array_row_types, array_column_to_index, array_data)

expect(array_row.dig("items", 0, "id")).to eq(1)
expect(array_row.dig("items", 1, "id")).to eq(2)
end
end

describe "#fetch" do
let(:row_types) do
[
{type: :text, scale: 0, precision: 0, name: :name},
{type: :fixed, scale: 0, precision: 0, name: :age}
]
end
let(:column_to_index) { {"name" => 0, "age" => 1} }
let(:data) { ["Alice", "30"] }
let(:row) { described_class.new(row_types, column_to_index, data) }

it "returns default value for missing key" do
expect(row.fetch("missing", "default")).to eq("default")
expect(row.fetch(:nonexistent, 0)).to eq(0)
end

it "yields to block for missing key" do
expect(row.fetch("missing") { |k| "no #{k}" }).to eq("no missing")
expect(row.fetch(:missing) { |k| "key: #{k}" }).to eq("key: missing")
end

it "raises KeyError for missing key without default or block" do
expect { row.fetch("missing") }.to raise_error(KeyError, /key not found/)
expect { row.fetch(:nonexistent) }.to raise_error(KeyError, /key not found/)
end
end
end
Loading