Same answer across backends
FeatureQL is one language. The compiler emits different SQL for DuckDB, Trino, and serving so the FeatureQL-visible result matches when the engine can do the work.
That is why a feature you authored on DuckDB can compile as a persisted view for Trino and as a prepared statement for serving without rewriting the formula. The engines still disagree on some algorithms and types; those stay honest errors or documented gaps, not silent different numbers.
Same FeatureQL, different SQL
These calls look like DuckDB. On Trino they become date_format, from_iso8601_timestamp (after a space→T rewrite), and json_extract_scalar. The values stay the same:
RETURN
iso := TO_ISO8601(DATE '2024-03-15'),
ts := FROM_ISO8601_TIMESTAMP('2024-03-15T10:30:00'),
name := JSON_EXTRACT_SCALAR(
JSON_PARSE('{"name":"Alice","age":30}'),
'$.name'
)
;| ISO VARCHAR | TS TIMESTAMP | NAME VARCHAR |
|---|---|---|
| 2024-03-15T00:00:00 | 2024-03-15T10:30:00 | Alice |
Use FeatureQL functions (TO_ISO8601(), FROM_ISO8601_TIMESTAMP(), JSON_EXTRACT_SCALAR()), not dialect builtins (strftime, from_iso8601_timestamp, json_extract). The compiler already owns the spelling.
Raw FROM SQL(...) inside EXTERNAL_COLUMNS() / BIND_COLUMNS() is the opposite contract: that string is engine SQL. Prefer INLINE_COLUMNS when the same query must run everywhere. Details: Mapping data sources .
What the compiler normalizes
When a backend is missing a builtin, or the builtin would change the answer, FeatureQL rewrites the tree:
| FeatureQL | What we do |
|---|---|
TO_ISO8601(DATE …) | Trino native to_iso8601 drops T00:00:00; we emit date_format so the string matches DuckDB |
TO_ISO8601(INTERVAL …) / MAKE_INTERVAL() | Trino cannot to_iso8601 an interval (and MAKE_INTERVAL is varchar prose); we build P…T… from parts |
FROM_ISO8601_TIMESTAMP() | Trino requires T; a space-separated string is rewritten first |
UTC_TO_LOCAL() | Naive timestamp fields are treated as UTC, then shifted — not as the session zone |
DATE_SUBTRACT(…, month/year) | Serving has no calendar date_sub; we use the same elapsed helper as Trino |
JSON '…' / DISPLAYS AS on JSON objects | Trino JSON_FORMAT is compact and key-sorted; object/array text is compared as JSON, not as pretty strings |
SQLTYPEOF() | Available on serving and Trino; the string is the engine's type name (next section) |
Purity and explicit casts are the language half of this story: Design principles . Types: Types .
What the engine still owns
These are not compiler bugs. No SQL tweak makes the engines agree, so FeatureQL either pins the difference, raises a stable UE/…, or leaves the function unavailable.
Approximate and even-count sketches. MEDIAN on an even count, APPROX_DISTINCT (HLL), and APPROX_PERCENTILE / Trino approx_percentile are engine sketches. DuckDB and Trino/serving can return different values on the same rows.
Last-bit floating point. E(), IEEE values past 2^53, and some interval displays differ by a ULP. FeatureQL DOUBLE is not a cross-engine exact decimal.
Types the engine does not have. BITSTRING is not a Trino type. Integer BITWISE_* exists; bitstring literals do not. Geospatial ST_* is parked on serving; demo Trino often has no spatial plugin.
Forms that need a different runtime. TRANSFORM(… METHOD CORRELATED) raises UE/TRANSFORM-NOT-IMPLEMENTED on Trino and serving. RECURSE() and window WF_* are not available there. Hybrid FEATUREQL(...) inside /* SQL */ is not supported on serving.
Planning cost. A valid graph can exceed Trino planning time or serving HTTP 408. That is a budget, not an unsupported function.
Storage spelling. TYPEOF() is FeatureQL. SQLTYPEOF() is whatever the engine prints (BIGINT on DuckDB, integer on Trino, Int64 on serving):
RETURN
fql := TYPEOF(1),
sql := SQLTYPEOF(1)
;| FQL VARCHAR | SQL VARCHAR |
|---|---|
| BIGINT | BIGINT |
If a query works on one backend and not another, compare TYPEOF / SQLTYPEOF and the structured UE/… code — not the generated SQL first. Inspection: Tools in the query .
How to prove it on your stack
The docs and playgrounds are the same SLT corpus. Run it on the backend you care about:
Conformance tests — client.sltest / POST /sltest with backend="trino" or "serving".
A persisted view compiles DuckDB, Trino, and serving artifacts together. If a formula is unavailable on one engine, create fails with that UE/… instead of storing a lie.
Next
Need a SQL table for BI? Persisted views . Need live FeatureQL inside SQL? Hybrid queries (DuckDB / Trino). Serving latency and sources: Real-time serving .