Hybrid queries
Hybrid queries let you embed FeatureQL inside SQL — or SQL inside FeatureQL — so a SQL client, BI tool, or application can use FeatureQL features without changing its workflow.
This page is the live-compilation door. For a frozen SQL table, use persisted views . For the whole edge, see SQL interoperability .
Language directives
When FeatureMesh receives a query, it needs to know which language to parse. Use comment directives to be explicit:
/* FEATUREQL */ -- Parse as FeatureQL
/* SQL */ -- Parse as SQL SQL, FEATUREQL, and STRICT share the same comment forms: /* TAG */ anywhere, or -- TAG as the first line when that comment is exactly the tag (-- SQL: note is an ordinary comment). Prefer the /* … */ form — that is what docs and error messages show.
Without a directive, FeatureMesh auto-detects the language. Explicit directives are safer, especially in hybrid queries where both languages appear.
Two embedding syntaxes
FeatureQL blocks can be embedded in SQL using two syntaxes:
| Syntax | Example | When to use |
|---|---|---|
| Parentheses (default) | FEATUREQL(RETURN ...) | Standard syntax — use this by default |
| Comment | FEATUREQL/* RETURN ... */ | For SQL clients that parse or introspect queries and choke on the parentheses form |
The parentheses syntax is cleaner and supports nesting. The comment syntax exists because some external tools (BI platforms, SQL linters, query formatters) inspect query text and may reject FEATUREQL(...) as invalid SQL. Wrapping the FeatureQL body in a comment (/* ... */) makes it invisible to those tools.
Both syntaxes produce identical results. Pick the one that works with your toolchain.
FeatureQL as a subtable
Wrap a FeatureQL query in FEATUREQL(...) to use it as a table expression inside SQL. The outer SQL can filter, join, or aggregate the results:
SELECT
STORE_IS_BCN,
COUNT() AS NUM
FROM FEATUREQL(
DEFINE
STORES := ENTITY(),
STORE_ID := INPUT(BIGINT#STORES),
DIM_STORE_ATTRIBUTES := EXTERNAL_COLUMNS(
store_id BIGINT#STORES BIND TO STORE_ID,
store_city VARCHAR
FROM SQL(SELECT store_id, store_city FROM (VALUES (1, 'BCN'), (2, 'BCN'), (3, 'MAD'), (4, 'MAD'), (5, 'VAL')) AS t(store_id, store_city))
),
STORE_CITY := DIM_STORE_ATTRIBUTES[store_city],
RETURN
STORE_ID := BIND_VALUES(ARRAY[1,2,3,4,5]),
STORE_CITY,
STORE_CITY='BCN' AS STORE_IS_BCN
)
GROUP BY (STORE_IS_BCN)| STORE_IS_BCN BOOLEAN | NUM BIGINT |
|---|---|
| false | 3 |
| true | 2 |
The FeatureQL block computes features for each store, and the outer SQL groups the results by STORE_IS_BCN — a pattern that's natural when your BI tool expects SQL but your metrics live in FeatureQL.
Metric additivity, units, and multiplicity are checked inside the FEATUREQL(...) island. An outer SQL SUM, AVG, or GROUP BY is ordinary SQL and is not re-validated. Keep rollups that must preserve meaning inside FeatureQL; use the outer SQL layer for presentation, client limits, and joins that your tool already owns.
FeatureQL as a CTE
FeatureQL blocks work as Common Table Expressions too. This separates feature computation from downstream analysis:
WITH
a AS FEATUREQL(
DEFINE
FEATURE1 := INPUT(BIGINT)
RETURN
FEATURE1 := BIND_VALUES(SEQUENCE(1,10)),
FEATURE2 := FEATURE1 * 2
)
SELECT * FROM a WHERE FEATURE2 < 6| FEATURE1 BIGINT | FEATURE2 BIGINT |
|---|---|
| 1 | 2 |
| 2 | 4 |
The CTE a computes features via FeatureQL, and the outer query filters with standard SQL. You can reference the same CTE multiple times in the main query, and the engine caches the results.
The same query using the comment syntax:
WITH
a AS FEATUREQL/*
DEFINE
FEATURE1 := INPUT(BIGINT)
RETURN
FEATURE1 := BIND_VALUES(SEQUENCE(1,10)),
FEATURE2 := FEATURE1 * 2
*/
SELECT * FROM a WHERE FEATURE2 < 6| FEATURE1 BIGINT | FEATURE2 BIGINT |
|---|---|
| 1 | 2 |
| 2 | 4 |
Working with SQL clients
Many SQL clients (Tableau, DBeaver, Superset) automatically wrap user queries:
SELECT * FROM (/* User's query */) LIMIT 1000 Hybrid syntax fits naturally into this pattern. The example below uses the comment syntax (FEATUREQL/* ... */), which is better suited here — SQL clients that introspect the outer query won't be confused by the embedded FeatureQL:
SELECT *, FEATURE1 + FEATURE2 AS SQL_COLUMN
FROM (
FEATUREQL /*
DEFINE
FEATURE1 := INPUT(BIGINT)
RETURN
FEATURE1 := BIND_VALUES(SEQUENCE(1,10)),
FEATURE2 := FEATURE1 * 2
*/
)
ORDER BY FEATURE1
LIMIT 3| FEATURE1 BIGINT | FEATURE2 BIGINT | SQL_COLUMN BIGINT |
|---|---|---|
| 1 | 2 | 3 |
| 2 | 4 | 6 |
| 3 | 6 | 9 |
The SQL_COLUMN in the outer query demonstrates that you can reference FeatureQL output columns in SQL expressions — the boundary between the two languages is transparent.
Compatible clients: Tableau, PowerBI, Looker, Superset, DataGrip, DBeaver, and any application using JDBC/ODBC.
Limitations
- FeatureQL does not optimize across language boundaries — each FeatureQL block is compiled independently, and filter pushdown depends on the underlying engine.
- Semantic checks (additivity, units, multiplicity) apply inside each
FEATUREQL(...)island. Outer SQL aggregates are ordinary SQL and are not re-validated. That is the language split, not a missing FeatureQL rule. - Hybrid queries are not supported on the DataFusion / serving backend. Same answer across backends lists that gap next to other engine-true limits.
- When possible, prefer pure FeatureQL for better maintainability and optimization. Use hybrid queries when you need to integrate with existing SQL workflows or tools that require SQL input.
A persisted view is the other SQL door: compile once, scan stored SQL. Hybrid recompiles the FeatureQL island on every request.
Next
Continue with persisted views when a BI tool should discover an ordinary table, or with the JDBC catalog when that tool lists schemas before it runs SQL.