Query language
OnchainQL contract
OnchainQueries accepts OnchainQL, an analytical SQL dialect, translates the parts it can execute safely, and rejects access outside the allowed catalog. Unsupported syntax fails explicitly rather than silently doing something else.
One read-only statement
Every request must contain exactly one query beginning with SELECT or WITH. A trailing semicolon is optional. OnchainQueries does not accept multiple statements, data modification, schema changes or session configuration.
Supported surface
- projections, expressions and aliases
WHERE,CASE,IN,BETWEENand ordinary predicatesDISTINCT,GROUP BY, aggregates andHAVINGORDER BYwithASC/DESCandNULLS FIRST/NULLS LASTLIMITandOFFSET- ordinary CTEs and subqueries
- common inner, left, right, full and cross joins
- common window functions and frames
UNION,INTERSECTandEXCEPTwhere the execution layer accepts the normalized form
Required time range
Every query must bound the time range it reads. Any SELECT that reads a source table needs a WHERE clause putting a lower bound on ts, so no query can scan all of history by omission.
SELECT count(*)FROM pump_fun.trade_eventWHERE ts >= TIMESTAMP '2026-08-01 00:00:00';An upper bound is not required — ts >= now() - INTERVAL '7' DAY is a complete bound. Any of these establish one:
| Form | Example |
|---|---|
>= | ts >= {{start_time}} |
> | ts > {{start_time}} |
| Reversed comparison | TIMESTAMP '2026-08-01' <= ts |
| Equality | ts = {{day}} |
BETWEEN | ts BETWEEN {{start}} AND {{end}} |
The bound must sit on the SELECT that reads the table, not on an enclosing query. In a CTE, subquery or derived table, put it inside:
WITH hourly AS ( SELECT date_trunc('hour', ts) AS hour, count(*) AS trades FROM pump_fun.trade_event WHERE ts >= {{start_time}} -- here, not on the outer SELECT GROUP BY 1)SELECT * FROM hourly ORDER BY hour;Each arm of a UNION, INTERSECT or EXCEPT is checked separately. A bound that only holds on one side of an OR does not count, because the other side is still unbounded. In a join, bound each side you want restricted.
Read-only restrictions
None of the following are allowed:
INSERT,UPDATE,DELETEand other data modificationCREATE,ALTER,DROPand other schema modificationSET,USE,SYSTEM, grants and administrative statements- user-controlled
FORMAT,SETTINGSorINTO OUTFILE - access to the execution layer’s internal tables
- external table functions such as
s3,url,file,postgresql,mysql,remoteandiceberg
VALUES relations and saved-query views such as query_123 are not part of the supported catalog. Submit requests, including SQL and parameter JSON, are limited to 2 MiB.
Filter, group and sort
SELECT mint, count(*) AS trades, sum(sol_amount) AS sol_volumeFROM pump_fun.trade_eventWHERE ts >= {{start_time}} AND ts < {{end_time}} AND is_buy = trueGROUP BY mintHAVING count(*) >= 10ORDER BY sol_volume DESC, mint ASCLIMIT 100;Multiple sort expressions, positional references such as ORDER BY 2 DESC, explicit null placement and LIMIT/OFFSET pagination all work. Rows tied on every ordering expression have no guaranteed relative order — add a unique tie-breaker when stable pagination matters.
CTEs, joins and windows
WITH hourly AS ( SELECT date_trunc('hour', ts) AS hour, sum(sol_amount) AS volume FROM pump_fun.trade_event WHERE ts >= {{start_time}} GROUP BY hour)SELECT hour, volume, sum(volume) OVER ( ORDER BY hour ROWS BETWEEN 23 PRECEDING AND CURRENT ROW ) AS rolling_24h_volumeFROM hourlyORDER BY hour;Use common JOIN ... ON or JOIN ... USING forms. Joins execute as one query rather than partitioned export tasks, so add selective time and key predicates to both sides.
Arrays and UNNEST
Array literals use bracket syntax: SELECT ARRAY[1, 2, 3]. The supported UNNEST form is a cross join over one column identifier with a table and column alias, which OnchainQueries rewrites to a an array join.
SELECT t.slot, path_indexFROM pump_fun.trade_event AS tCROSS JOIN UNNEST(t.ix_path) AS p(path_index)WHERE t.ts >= {{start_time}};TRY_CAST
OnchainQueries evaluates TRY_CAST through a local evaluation step so invalid values become NULL.
SELECT TRY_CAST(mint AS BIGINT) AS mint_numberFROM pump_fun.trade_eventWHERE slot > 0;Export efficiently
A simple single-table projection can be divided into independent daily tasks when it has no CTE, join, distinct, grouping, ordering, limit, offset, window or set operation, and includes ts in the output. This is the most efficient shape for large exports.
SELECT ts, slot, sig, mint, sol_amountFROM pump_fun.trade_eventWHERE ts >= {{start_time}} AND ts < {{end_time}};- constrain
tswhenever possible - select only required columns
- aggregate before sorting large result sets
- use
LIMITfor exploratory top-N queries - omit a global
ORDER BYwhen downstream tools can sort the exported parts - prefer
--engine autofirst, then select a larger class only when necessary
Coming from another engine
Port incrementally: verify the table coverage, replace quoted parameters, start with the core projection and filters, then add functions, joins, windows and arrays one at a time. A query succeeding because a function with the same name happens to exist underneath does not guarantee OnchainQL argument, null, type-coercion or return-type semantics.