Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0e858e6
fix: lead each shared SQLGetTypeInfo DATA_TYPE with its plain type
adwk67 Oct 6, 2026
c0f4def
chore: bump rustls to 0.23.45 for RUSTSEC-2026-0285
adwk67 Oct 6, 2026
dd31b43
fix: quote and escape VARCHAR constants in the connector's Constant v…
adwk67 Oct 7, 2026
494c1cd
test: seed a hive view with INTERVAL and date/time columns for Power …
adwk67 Oct 7, 2026
c8a84a2
test: add a Power BI slicer-semantics acceptance suite (currently fai…
adwk67 Oct 7, 2026
4bde7ee
fix: deliver interval columns as Trino's own text
adwk67 Oct 7, 2026
099590e
fix: stop Power BI folding time slicers that can never match
adwk67 Oct 7, 2026
d7da704
fix: deliver timestamp with time zone in the session time zone
adwk67 Oct 7, 2026
34fc424
docs: add changelog entries for the type-info and connector quoting f…
adwk67 Oct 7, 2026
a9f7617
test: run Windows suites with UTF-8 output
adwk67 Oct 8, 2026
44c1da7
fix: deliver time with time zone in the session zone too
adwk67 Oct 8, 2026
fe0e43f
chore: pin stackable-odbc-core to 2f9f725
adwk67 Oct 8, 2026
6e83771
test: pull MinIO from pgsty instead of quay.io
adwk67 Oct 9, 2026
7c2722d
test: listen on a fixed port in the HYT00 timeout test
adwk67 Oct 9, 2026
e7fab77
chore: pin stackable-odbc-core to 53e5a89
adwk67 Oct 9, 2026
e1b3c91
chore: pin stackable-odbc-core to the v0.1.1 release
adwk67 Oct 9, 2026
a466d0b
test: refuse to run the Windows suites with ODBC tracing on
adwk67 Oct 9, 2026
e26349e
test: keep Trino's config visible and current when setup.sh re-runs
adwk67 Oct 9, 2026
c7c0b81
docs: describe the tracing slowdown without test-machine numbers
adwk67 Oct 9, 2026
197cf66
docs: note that incremental-refresh bounds follow the session time zone
adwk67 Oct 9, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ jobs:
# per layer: once a single layer exhausts it, the pull fails and takes
# every other image with it. Layers that did arrive stay in the local
# content store, so a second attempt refetches only what is missing and
# costs seconds. The spooling leg is the one that reaches quay.io for
# MinIO, and that is where the dropped connections have been seen.
# costs seconds. The spooling leg pulls the most, MinIO on top of the
# core images, so it is the one most exposed to a dropped connection.
- name: Pull the stack images
# From the stack directory, as scripts/lib.sh's `compose` wrapper does.
# Compose derives the project name from the compose file's directory,
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `timestamp with time zone` values are delivered as wall time in the session
time zone instead of in UTC: `TimeZone=` when set, otherwise the
coordinator's default, and whatever a later `SET TIME ZONE` chose. Power BI
folds the value it showed back as a plain `TIMESTAMP` literal, which Trino
reads in the session zone, so a DirectQuery slicer on such a column only
selected its rows in a UTC session. Applications reading these columns see
different values unless the session is UTC. `time with time zone` values
follow the same zone, at its current offset because a time has no date, which
is how Trino itself casts `TIME` to `TIME WITH TIME ZONE`.
- The Power BI connector reports that Trino cannot convert `TIME` to
`TIMESTAMP` (`SQL_CONVERT_TIME` without `SQL_CVT_TIMESTAMP`). A DirectQuery
slicer on a time column folded to a comparison against Power BI's base date,
30 December 1899, while Trino anchors a cast time on the current date, so the
report silently showed no rows. Power BI now refuses that fold with a visible
error instead. The README describes a view-based workaround.

### Fixed

- `SQLGetTypeInfo` lists the plain type first among the rows that share a
`DATA_TYPE`: `VARCHAR` for `SQL_WVARCHAR`, `TIME` for `SQL_TYPE_TIME` and
`TIMESTAMP` for `SQL_TYPE_TIMESTAMP`, as the spec's "how closely the data type
maps" ordering requires. The rows were sorted by name, so `SQL_WVARCHAR` led
with `INTERVAL DAY TO SECOND`, and Power Query, which takes the first row as
its `CAST` target, folded a DirectQuery slicer on a text column into
`CAST(... AS INTERVAL DAY TO SECOND)`, which Trino rejected.
- The Power BI connector quotes text constants and doubles any `'` in them.
Power Query hands the constant over unquoted, so a slicer value folded into
`CAST(hello world as VARCHAR)` and failed. Together with the `SQLGetTypeInfo`
fix above this makes DirectQuery slicers on text columns work, so the driver
and the connector have to be upgraded together.
- `INTERVAL YEAR TO MONTH` and `INTERVAL DAY TO SECOND` columns read as text now
return Trino's own rendering, the same text `CAST(... AS VARCHAR)` produces
(`-1-0`, `0 00:00:00.500`). They were parsed into fields and re-rendered
(`-1-00`, `0 00:00:00.5`), so a Power BI DirectQuery slicer on an interval
column, which folds to `cast(col as VARCHAR) = '<shown value>'`, silently
selected no rows. Reading these columns as `SQL_C_INTERVAL_*` still works:
stackable-odbc-core now converts interval text to those C types.

## [0.1.2] — 2026-09-01

### Changed
Expand Down
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ snafu = "0.9"
# reviewable edit to this line instead of whatever the branch happens to point
# at. To build against a local checkout, use a `[patch]` in your own
# `.cargo/config.toml` rather than editing this line; see CONTRIBUTING.md.
stackable-odbc-core = { git = "https://github.com/stackabletech/stackable-odbc-core.git", tag = "v0.1.0" }
stackable-odbc-core = { git = "https://github.com/stackabletech/stackable-odbc-core.git", tag = "v0.1.1" }
# `time` is needed directly by `query_all_rows_within`, which bounds the login
# round trip with `tokio::time::timeout`. It resolves without being declared,
# because reqwest enables it, but a direct use must not rely on another crate's
Expand All @@ -60,7 +60,7 @@ serial_test = "4"
# offline FFI tests need. It is default-off because it is test code, so it is
# enabled here rather than on the [dependencies] entry above -- that keeps it
# out of the shipped cdylib.
stackable-odbc-core = { git = "https://github.com/stackabletech/stackable-odbc-core.git", tag = "v0.1.0", features = ["test-support"] }
stackable-odbc-core = { git = "https://github.com/stackabletech/stackable-odbc-core.git", tag = "v0.1.1", features = ["test-support"] }

[lints.clippy]
unwrap_in_result = "deny"
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ proper entry in the **Get Data** dialog instead of the generic ODBC one.
2. In **File > Options > Security**, allow any extension to load.
3. Restart Power BI Desktop. **Stackable Trino** now appears under **Get Data**.

For incremental refresh on a `timestamp with time zone` column, Trino compares
the `RangeStart` and `RangeEnd` bounds in the session time zone (`TimeZone`),
so partition boundaries follow that zone. Changing `TimeZone` on a dataset that
already has partitions moves the boundaries.

### Your first query

Assuming a Trino instance is reachable on the given host and port:
Expand Down Expand Up @@ -148,7 +153,7 @@ The authoritative list is `src/backend/types/connect_params.rs`.
| `Roles` | No | Authorisation role per catalog, `{catalog:role;catalog2:ALL}` |
| `SessionUser` | No | User statements run as, while `User` still authenticates. JDBC's `sessionUser` |
| `Path` | No | Default SQL path for resolving unqualified function names |
| `TimeZone` | No | IANA session time zone (`Europe/Berlin`). Unset leaves the coordinator's |
| `TimeZone` | No | IANA session time zone (`Europe/Berlin`). Unset leaves the coordinator's default. `timestamp with time zone` and `time with time zone` values are delivered as wall time in the session zone, which a later `SET TIME ZONE` changes |
| `Locale` | No | Locale for locale-dependent formatting, sent as `X-Trino-Language` |
| `ClientInfo` | No | Free-form client metadata Trino records against the query |
| `TraceToken` | No | Correlation token Trino records against the query |
Expand Down Expand Up @@ -259,6 +264,19 @@ ignored, so the tool can react instead of trusting a wrong answer.
somewhere you never asked for. Set `Catalog` when you connect.
- **Row and field size limits are not faked.** Trino can only limit a result set
through `LIMIT` in the SQL you wrote.
- **Power BI cannot filter on a time column in DirectQuery.** Picking a value
in a slicer on a `time` column fails with "We couldn't fold the expression to
the data source". Power BI would filter by casting the column to a timestamp
and comparing it with that time on 30 December 1899, while Trino, like ODBC
itself, puts a cast time on today's date, so the filter could never match.
The connector declares the cast unsupported so you see an error instead of an
empty report. To slice on a time of day, expose it as text in a Trino view,
for example `CAST(col_time AS VARCHAR) AS col_time_text`, and slice on that
column. Selecting "(Blank)" still works. Users who cannot create views can
switch the table to Import mode, where the slicer filters Power BI's own copy
of the data. That copy is only as fresh as its last refresh, and Power BI
treats a blank time as equal to midnight, so selecting 12:00:00 AM also shows
rows without a time.
- **One isolation level.** Trino catalogs disagree about which levels they
accept, so the driver offers the one they all support and refuses the rest up
front, rather than letting a query fail later for a reason nobody can see.
Expand Down Expand Up @@ -307,6 +325,22 @@ the coordinator's chain is refused even when the machine trusts that chain.
**Only the first session property applies.** Wrap the value in braces. See
[Values that contain a semicolon](#values-that-contain-a-semicolon).

**A Power BI slicer misses a timestamp from the night the clocks go back.**
`timestamp with time zone` values are shown in the session time zone, and in
the hour that repeats, two instants share one wall time. Power BI filters on the
wall time it showed and Trino reads that as the later of the two instants, so a
value from the first of the repeated hours is not selected.

**Large reads fail with `ABANDONED_QUERY` or `Query not found`.** Trino
abandons a query whose results the client has not fetched within
`query.client.timeout` (5 minutes by default), and later forgets it altogether,
after which the next fetch returns `404 Not Found: Query not found`. A client
that reads slowly enough gets there on a large result. One cause on
Windows is ODBC tracing left switched on: it writes every call to a file and
slows reads down considerably. Turn it off in the ODBC Data Source
Administrator (Tracing tab, **Stop Tracing Now**), including on an on-premises
data gateway.

**The browser login never opens.** Some tools, `pyodbc` among them, tell the
driver it may not display anything. The driver reports this rather than hanging.
Use `AccessToken` with those tools, or connect through one that allows a prompt.
Expand Down
41 changes: 30 additions & 11 deletions connector/StackableTrinoODBC.pq
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ StackableTrinoODBCImpl = (
SupportsTop = false
],

// Nothing is overridden. An override here silently wins over
// SQLGetInfoW and cannot be corrected by fixing the driver, so the
// record is reserved for what the driver gets wrong, and this group it
// answers honestly: SQL_SQL92_PREDICATES, SQL_AGGREGATE_FUNCTIONS,
// One entry is overridden, SQL_CONVERT_TIME, below. An override here
// silently wins over SQLGetInfoW and cannot be corrected by fixing the
// driver, so the record is otherwise reserved for what the driver gets
// wrong, and this group it answers honestly: SQL_SQL92_PREDICATES, SQL_AGGREGATE_FUNCTIONS,
// SQL_SQL92_RELATIONAL_JOIN_OPERATORS, SQL_SQL92_VALUE_EXPRESSIONS and
// SQL_IDENTIFIER_QUOTE_CHAR.
//
Expand All @@ -327,7 +327,22 @@ StackableTrinoODBCImpl = (
// Nothing Power Query generates is lost: comparison, IN,
// LIKE, BETWEEN, IS NULL, EXISTS and the four join types are all in
// the driver's ungated set.
SQLGetInfo = defaultConfig[SQLGetInfo],
//
// SQL_CONVERT_TIME is the driver's answer (every SQL_CVT_* bit) minus
// SQL_CVT_TIMESTAMP, and that is a deliberate misreport: Trino can
// cast TIME to TIMESTAMP. It anchors the time on the current date, as
// ODBC's own conversion tables do ("SQL to C: Time", footnote [c]),
// while Power BI folds a time slicer to
// `cast(col as TIMESTAMP) = CAST('1899-12-30 hh:mm:ss' as TIMESTAMP)`,
// anchoring on its own base date. That filter ran and silently matched
// no row. Without the bit, Power BI refuses the fold and says so ("We
// couldn't fold the expression to the data source"), which is the
// honest outcome for a filter Trino cannot evaluate the way Power BI
// means it. test_folding_contract.py pins the value to the driver's
// answer and checks Trino's anchor is still the current date.
SQLGetInfo = defaultConfig[SQLGetInfo] & [
SQL_CONVERT_TIME = 0x01FDFFFF
],

SQLColumns = (catalogName, schemaName, tableName, columnName, source) =>
source,
Expand Down Expand Up @@ -394,12 +409,16 @@ StackableTrinoODBCImpl = (
// rendering it as Trino's `X'..'` literal
// cannot be verified without Power BI Desktop.
//
// Power Query hands a text-valued constant to the visitor
// bare, not quoted: confirmed in Power BI Desktop on
// 2026-10-07, where `Cast(_, "VARCHAR")` folded a slicer
// into `CAST(hello world as VARCHAR)`. A text entry therefore
// quotes the value itself and doubles any single quote in
// it, or `O'Brien` ends the literal early.
//
// TODO: add UUID, JSON, TIME WITH TIME ZONE and TIMESTAMP
// WITH TIME ZONE. Each has a valid Trino cast, but the
// rendering turns on whether Power Query hands a
// text-valued constant to the visitor already quoted, which
// the VARCHAR entry below also rests on. Confirm that in
// Power BI Desktop, then add all four.
// WITH TIME ZONE, now that the quoting question above is
// settled. Each has a valid Trino cast.
Visitor = [
DECIMAL = each Cast(_, "DECIMAL"),
INTEGER = each Cast(_, "INTEGER"),
Expand All @@ -410,7 +429,7 @@ StackableTrinoODBCImpl = (
DOUBLE = each Cast(_, "DOUBLE PRECISION"),
BOOLEAN = each Cast(_, "BOOLEAN"),
DATE = each Cast(Quote(Date.ToText(_, "yyyy-MM-dd")), "DATE"),
VARCHAR = each Cast(_, "VARCHAR"),
VARCHAR = each Cast(Quote(Text.Replace(_, "'", "''")), "VARCHAR"),
// `fffffff`, not `sssssss`: in a custom format string
// `s` is the second and `f` is the fractional second,
// so the latter spelling renders the second eight times
Expand Down
4 changes: 2 additions & 2 deletions fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ one: a value a coordinator legitimately sent must fail safe, and where a
release build has no overflow checks the same defect returns a wrong answer
instead of an error.

- `json_value` covers `json_to_column_value` and the dozen temporal, interval
and decimal scanners under it. This is the half of the read path core does
- `json_value` covers `json_to_column_value` and the temporal and decimal
scanners under it. This is the half of the read path core does
not see: core fuzzes `write_column_value`, which turns the resulting
`ColumnValue` into the caller's buffer, and nothing covered the step that
produces it.
Expand Down
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/json_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use trino_rust_client::{TrinoFloat, TrinoInt, TrinoTy};
//
// stackable-odbc-core already fuzzes the second half (`write_column_value`,
// ColumnValue -> the caller's buffer). Nothing covered the step before it,
// which is where this crate's temporal, interval and decimal parsers live:
// roughly a dozen hand-written scanners over text a Trino coordinator chose.
// which is where this crate's temporal and decimal parsers live: hand-written
// scanners over text a Trino coordinator chose.
// Every one of them runs on the server's side of the trust boundary.
//
// The property is that no input panics. A panic here is caught at the FFI
Expand Down
7 changes: 6 additions & 1 deletion integration-tests/scripts/gen-trino-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh"

OUT="$GENERATED/trino"
rm -rf "$OUT"
# Emptied in place, never removed: a running coordinator bind-mounts this
# directory, and replacing it would leave the container holding the deleted
# one, empty, so a restart would find no config at all. setup.sh recreates
# Trino when the assembled content changes (TRINO_CONFIG_HASH).
mkdir -p "$OUT"
find "$OUT" -mindepth 1 -delete
mkdir -p "$OUT/catalog"

cp -r "$STACK_DIR/trino/base/." "$OUT/"
Expand Down
Loading
Loading