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:

FeatureQL
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'
    )
;
Result
ISO VARCHARTS TIMESTAMPNAME VARCHAR
2024-03-15T00:00:002024-03-15T10:30:00Alice

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:

FeatureQLWhat 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 objectsTrino 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):

FeatureQL
RETURN
    fql := TYPEOF(1),
    sql := SQLTYPEOF(1)
;
Result
FQL VARCHARSQL VARCHAR
BIGINTBIGINT

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 .