Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Added

* Support named and positional bind values through `QueryRequest#params`.

## [0.7.0](https://github.com/altertable-ai/altertable-lakehouse-ruby/compare/altertable-lakehouse/v0.6.0...altertable-lakehouse/v0.7.0) (2026-08-26)


Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ client.append(

# Query data
result = client.query_all(
statement: "SELECT * FROM main.public.events LIMIT 10"
statement: "SELECT * FROM main.public.events WHERE age >= $min_age",
params: { "min_age" => 25 }
)
result[:rows].each { |row| puts row }
```
Expand Down
17 changes: 15 additions & 2 deletions lib/altertable/lakehouse/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ class QueryResult
include Enumerable

# metadata: the stream header object (first NDJSON line)
# columns: array of column name strings (second NDJSON line)
# columns: array of column names or typed column descriptors (second NDJSON line)
attr_reader :metadata, :columns

def initialize(enum)
Expand All @@ -304,7 +304,7 @@ def each(&block)
when 0
@metadata = item
when 1
@columns = item
@columns = normalize_columns(item)
else
if @columns.is_a?(Array) && item.is_a?(Array)
block.call(@columns.zip(item).to_h)
Expand All @@ -315,6 +315,19 @@ def each(&block)
line_index += 1
end
end

private

# The query service emits typed column descriptors, such as
# { "name" => "id", "type" => "INTEGER" }. Keep the public result
# shape stable by exposing names and using them as row keys.
def normalize_columns(columns)
return columns unless columns.is_a?(Array)

columns.map do |column|
column.is_a?(Hash) && column.key?("name") ? column["name"] : column
end
end
end

end
Expand Down
6 changes: 4 additions & 2 deletions lib/altertable/lakehouse/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ def self.from_h(h)
end

class QueryRequest < Request
attr_reader :statement, :catalog, :schema, :session_id, :compute_size, :sanitize, :limit, :offset, :timezone, :ephemeral, :visible, :requested_by, :query_id, :cache
attr_reader :statement, :catalog, :schema, :session_id, :compute_size, :sanitize, :limit, :offset, :timezone, :ephemeral, :visible, :requested_by, :query_id, :cache, :params

def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_size: nil, sanitize: nil, limit: nil, offset: nil, timezone: nil, ephemeral: nil, visible: nil, requested_by: nil, query_id: nil, cache: nil)
def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_size: nil, sanitize: nil, limit: nil, offset: nil, timezone: nil, ephemeral: nil, visible: nil, requested_by: nil, query_id: nil, cache: nil, params: nil)
@statement = statement
@catalog = catalog
@schema = schema
Expand All @@ -71,6 +71,7 @@ def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_s
@requested_by = requested_by
@query_id = query_id
@cache = cache
@params = params
end

def to_h
Expand All @@ -88,6 +89,7 @@ def to_h
h[:requested_by] = @requested_by if @requested_by
h[:query_id] = @query_id if @query_id
h[:cache] = @cache unless @cache.nil?
h[:params] = @params unless @params.nil?
h
end
end
Expand Down
8 changes: 6 additions & 2 deletions rbi/altertable/lakehouse.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ module Altertable
sig { returns(T.nilable(T::Boolean)) }
def cache; end

sig { returns(T.nilable(T.any(T::Hash[String, T.untyped], T::Array[T.untyped]))) }
def params; end

sig do
params(
statement: String,
Expand All @@ -315,10 +318,11 @@ module Altertable
visible: T.nilable(T::Boolean),
requested_by: T.nilable(String),
query_id: T.nilable(String),
cache: T.nilable(T::Boolean)
cache: T.nilable(T::Boolean),
params: T.nilable(T.any(T::Hash[String, T.untyped], T::Array[T.untyped]))
).void
end
def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_size: nil, sanitize: nil, limit: nil, offset: nil, timezone: nil, ephemeral: nil, visible: nil, requested_by: nil, query_id: nil, cache: nil); end
def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_size: nil, sanitize: nil, limit: nil, offset: nil, timezone: nil, ephemeral: nil, visible: nil, requested_by: nil, query_id: nil, cache: nil, params: nil); end

sig { returns(T::Hash[Symbol, T.untyped]) }
def to_h; end
Expand Down
4 changes: 3 additions & 1 deletion sig/altertable/lakehouse/models.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module Altertable
attr_reader requested_by: String?
attr_reader query_id: String?
attr_reader cache: bool?
attr_reader params: (::Hash[String, untyped] | ::Array[untyped])?

def initialize: (
statement: String,
Expand All @@ -60,7 +61,8 @@ module Altertable
?visible: bool?,
?requested_by: String?,
?query_id: String?,
?cache: bool?
?cache: bool?,
?params: (::Hash[String, untyped] | ::Array[untyped])?
) -> void

def to_h: () -> ::Hash[Symbol, untyped]
Expand Down
25 changes: 23 additions & 2 deletions spec/altertable/lakehouse_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@
# ── #query ───────────────────────────────────────────────────────────────────

describe "#query (streaming)" do
it "normalizes typed column descriptors to column names" do
stream = [
{ "statement" => "SELECT 1 AS id" },
[{ "name" => "id", "type" => "INTEGER" }],
[1]
]
result = Altertable::Lakehouse::QueryResult.new(stream.each)

expect(result.to_a).to eq([{ "id" => 1 }])
expect(result.columns).to eq(["id"])
end

it "parses the header, column names and data rows from a SELECT" do
result = client.query(statement: "SELECT 42 AS answer")
rows = result.to_a
Expand Down Expand Up @@ -173,8 +185,8 @@
)
rows = result.to_a

# The mock returns [] as the columns array when DuckDB produces no batches
expect(result.columns).to eq([])
# Column metadata is present even when the query produces no rows.
expect(result.columns).to eq(["n"])
expect(rows).to be_empty
end
end
Expand All @@ -201,6 +213,15 @@

expect(result[:rows]).to eq([{ "cached_value" => 1 }])
end

it "serializes named bind parameters" do
request = Altertable::Lakehouse::Models::QueryRequest.new(
statement: "SELECT $min_age",
params: { "min_age" => 25 }
)

expect(request.to_h).to include(params: { "min_age" => 25 })
end
end

# ── custom headers ───────────────────────────────────────────────────────────
Expand Down
Loading