QQL is the structured definition behind every visual question in Qrly — columns, calculated fields, a nested
AND/OR filter tree, joins, aggregations, breakouts, having, ordering. Around 35 filter operators, a 19-category
type system, and a compiler that knows the difference between OFFSET…FETCH NEXT and TOP n.
Three things worth knowing before the reference tables.
", backtick or [ ]LIMIT/OFFSET vs OFFSET…FETCH NEXT vs TOP nEverything a question is, in a form a machine can validate, an agent can propose and a reviewer can read.
The source is either a physical table or question:<id> — a saved question compiled as a subquery. Columns are selected explicitly, each optionally carrying an alias, a raw expression, or a computed function, so a query document is never a SELECT * that quietly changes meaning when someone adds a column upstream.
Not a flat list of conditions joined by a single connective. A tree, so (A or B) and not (C and D) is expressible as itself rather than as an approximation. The same structure the visual builder edits is the one the compiler reads and the one an agent proposes.
Five aggregations — COUNT (including COUNT(*)), SUM, AVG, MIN, MAX — as a strict whitelist. Breakouts produce the grouping, having filters the groups, and order-by, limit and offset finish the shape. The OLAP compiler adds COUNT(DISTINCT …) and case-conditional measures on top.
prefer_timezone, applied at the sessionAn IANA timezone travels with the query document and is applied as a session-level statement, or supplied per request through a Prefer: header. Date bucketing, day boundaries and "yesterday" then mean what the reader expects them to mean, rather than what the database server's clock happens to think.
LEFTINNERRIGHTFULLquestion:<id>(field IS NULL) emulation on MySQL, MariaDB, SQLite and Sybase
Around 35 operators. The filter interface only offers the ones the column's type actually supports — which is why the type system on the next section matters as much as this list does.
= != > >= < <=IS NULL · IS NOT NULLIS DISTINCT FROM · IS NOT DISTINCT FROM!= drop my NULL rows".IN · NOT INBETWEENDATE_RANGE widget.CONTAINS · STARTS_WITH · ENDS_WITHLIKE · ILIKEILIKE compiles natively where it exists, otherwise via LOWER(), otherwise plain — per dialect.MATCH · IMATCHCONTAINS cannot express.ANY · ALLCS @>CD <@CS.OV &&SL <<SR >>NXR &<NXL &>ADJ -|-HAS_ELEMENTJSON_PATH · JSON_PATH_TEXTAT_TIMEZONEprefer_timezone.PAST_N_DAYSSTART_OF_MONTHFTSto_tsquery — the full query syntax, operators and all.PLFTSplainto_tsquery — plain words, ANDed.PHFTSphraseto_tsquery — words in order.WFTSwebsearch_to_tsquery — quotes and minus signs, the way a search box behaves.cast drawn from a 30-entry allow-list
A raw flag for verbatim SQL
A computed flag for Postgres row-type functions
today, tomorrow, yesterday, infinity, -infinity, epoch and now are understood as literals in a filter value. They compile into whatever each engine wants them to be, so a filter written against Postgres does not have to be re-authored when the same question is pointed at BigQuery.
raw, computed and castQQL does not pretend to cover every expression a database can evaluate. A filter can carry verbatim SQL through the raw flag, address Postgres row-type functions through computed, and coerce a value through a cast — but the cast comes from a fixed 30-entry allow-list rather than being a free string, and every alias and join reference is validated against an identifier regex.
The reason the filter interface offers the right operators on a MySQL tinyint(1) and on a Snowflake variant without the user knowing either detail.
TEXT · NUMBER · BOOLEANDATE · TIMESTAMP · TIMESTAMPTZ · INTERVALJSON · JSONB · ARRAY · RANGE · COMPOSITEENUM · UUID · BIT · BYTEAbyte[] is returned Base64-encoded rather than mangled.XML · GEOMETRY · UNKNOWN_text arrays · MySQL tinyint(1), enum, set
SQL Server bit, datetimeoffset · BigQuery struct, bytes
Snowflake variant, timestamp_ltz · SQLite affinity rules
JDBC drivers report display sizes that are frequently meaningless — a column declared once and reported as two billion characters wide. The normaliser suppresses those rather than rendering them, so the schema browser shows what a person can use.
Arrays become lists. byte[] becomes Base64. Postgres json and jsonb become parsed structures rather than strings that happen to look like JSON. Range types become structured maps, XML becomes a string, and every date becomes ISO-8601 — one shape, whatever the driver felt like returning.
Reference a saved question with question:<id> as a source table or a join target and it compiles as a subquery — a curated, reviewed result set becomes a building block instead of a copy-paste. Qrly detects cycles and caps nesting at a depth of 5, because an unbounded chain of derived questions is a compile-time hazard, not a feature.
ROWS strips the aggregations to expose the rows behind a number. FILTER keeps the aggregation and narrows to the group you clicked. On a visual definition this happens structurally on the query document; on native SQL it happens by clause surgery.
And where clause surgery is not safe — CTEs, nested SELECTs — it refuses. A drill-down that quietly rewrites a query it does not understand is worse than no drill-down, because the number it returns still looks plausible.
A flat SELECT converts into a visual definition — joins, filters, group by, having, order by with NULL ordering, limit and offset. Anything that cannot survive the trip is reported as a lossy-feature warning: CTEs, UNION, subqueries. You are told what was left behind before you save over the original.
Mark columns filterable, define lookup SQL to populate the dropdown — optionally against a different connection — and set runtime labels. Widgets are SELECT, INPUT, DATE, DATE_RANGE and NUMBER, with multi-select, required and default value.
On a visual question the filter becomes an extra QQL filter; on a native question it substitutes a {{param}}. AI can suggest both the filters and the lookup SQL — as a suggestion, which you accept or ignore.
Define a virtual column from a SQL expression with a type hint, then use it in filters, breakouts, having, order-by and aggregations like any other column. Validation happens on the server with an explicit Valid/Invalid status, edited in a Monaco mini-editor with column completion, and there is an AI chat for authoring the expression when the syntax is not the interesting part of your day.
{{template_variables}} as prepared statementsNative SQL template variables are bound as prepared-statement parameters, typed from their declaration or from context. Not string interpolation with escaping bolted on — actual parameter binding, which is the only version of this feature that is safe to hand to a dashboard viewer.
Every layer is a whitelist or an allow-list. None of them is a blacklist of things we thought to ban.
SELECT or WITH. Beyond that opening rule it blocks DROP, DELETE, INSERT, UPDATE, TRUNCATE, ALTER, CREATE, GRANT, REVOKE, EXECUTE, EXEC, CALL, MERGE, REPLACE, LOAD, IMPORT, EXPORT, COPY, VACUUM, ANALYZE, LISTEN, NOTIFY, LOCK, SET, RESET, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, DO, DECLARE and REFRESH. Schema changes have their own deliberate, separate path — they do not arrive through a question.EXPLAIN and EXPLAIN (FORMAT JSON), count queries, and compile-only preview — so "what would this do" is answerable before "do it".Large results stream as NDJSON with meta, row, end, truncated and error frames, honouring the driver fetch size, with a live row counter, a progress bar, and a cancel that genuinely aborts the request. CSV export streams too, rather than assembling the whole file in memory first.
Autocomplete that knows your tables is the difference between an editor and a text box with syntax colouring.
alias.column dot completion, tables after FROM and JOIN, columns after SELECT and WHERE, plus a SQL function catalogue with signatures and documentation, and keywords.QQL is Qrly's query document: a structured definition holding a source table (or a reference to another question), explicit column selections with alias, raw expression or computed function, calculated fields, a nested AND/OR filter tree with negation, aggregations, breakouts, having, order-by, limit, offset, joins and a preferred IANA timezone.
The compiler turns that document into SQL for whichever of the 12 dialect strategies the connection uses, so the same question runs on PostgreSQL, BigQuery, Snowflake or SQL Server without being rewritten.
Not through a question. The SQL sanitizer requires native SQL to start with SELECT or WITH, and blocks DROP, DELETE, INSERT, UPDATE, TRUNCATE, ALTER, CREATE, GRANT, REVOKE, EXECUTE, EXEC, CALL, MERGE, REPLACE, LOAD, IMPORT, EXPORT, COPY, VACUUM, ANALYZE, LISTEN, NOTIFY, LOCK, SET, RESET, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, DO, DECLARE and REFRESH.
Calculated-field expressions go through a separate sanitizer, and aliases and join references are validated against an identifier regex with a cast allow-list. Schema authoring has its own deliberate, separate path.
Around 35, grouped as comparison, null, null-safe, sets, text (including LIKE, ILIKE, and the regex operators MATCH and IMATCH), quantified ANY and ALL, JSON/array/range containment and adjacency on Postgres and Redshift, array membership, JSON path, temporal helpers, and four full-text search operators — FTS, PLFTS, PHFTS and WFTS — each taking a validated optional language argument.
Each filter may also carry a cast drawn from a 30-entry allow-list, a raw flag for verbatim SQL, or a computed flag for Postgres row-type functions.
Yes, for a flat SELECT. Qrly reverse-engineers it into a visual definition covering joins, filters, group by, having, order by with NULL ordering, and limit/offset. Features that cannot survive the conversion — CTEs, UNION, subqueries — are reported as explicit lossy-feature warnings rather than silently dropped.
The reverse direction is always available too: any visual question compiles to SQL you can read, and the editor toolbar converts between the two.
Yes, within limits it states honestly. Drill-down has two modes: ROWS, which strips the aggregations to expose the underlying rows, and FILTER, which keeps the aggregation and narrows to the group you clicked. On visual definitions it operates structurally on the query document; on native SQL it works by clause surgery.
Where clause surgery would be unsafe — CTEs, nested SELECTs — it refuses rather than mangling a query it cannot rewrite correctly.
The compiler emulates it. Order-by supports ASC/DESC with NULLS FIRST, NULLS LAST or the database default, including ordering on computed fields, and on MySQL, MariaDB, SQLite and Sybase it automatically emits a leading (field IS NULL) expression to produce the same ordering.
That is one instance of a general pattern: per dialect the compiler also handles identifier quoting, LIMIT/OFFSET versus OFFSET…FETCH NEXT versus TOP n, and the right date-bucketing spelling.
A query document your analysts can build visually, your engineers can read as SQL, and your agents can propose — with the same sanitizer in front of all three.