RiverQLDocs
Reference
Lv.1 · 0% done0 XP
0/8 sections
Universal Query Language

RiverQL Reference

Query PostgreSQL, MySQL, SQLite, MongoDB, and SQL Server through a single consistent syntax. Read the docs, earn XP, and collect badges as you learn.

Total XP:0
Sections Complete:0 / 8
Badges Earned:0 / 8
1
Section 1· +100 XP on complete

Query Basics

Every RiverQL query starts with find. It's the universal entry point that maps to SELECT in SQL. Write once, run on PostgreSQL, MySQL, SQLite, MongoDB, or SQL Server.

find — SELECT

Retrieve all columns from a table. The simplest possible query.

riverql
find users

Equivalent to SELECT * FROM users in standard SQL.

Column Selection

Use square brackets to pick specific columns instead of fetching everything.

riverql
find [name, email] from users
combined
find [name, email, department] from users
where status = "active"

Filtering with where

Filter rows using where. Chain conditions with and / or.

basic filter
find users where status = "active"
chained
find users where status = "active" and age > 21

Sorting with order by

Sort results ascending or descending with order by.

single sort
find [name, salary] from users
order by salary desc
multi-key sort
find [name, department, salary] from employees
order by department asc, salary desc

Limiting & Pagination

Control result count with limit and paginate using offset.

limit
find users limit 10
pagination — page 3 of 20-row pages
find [name, email] from users
order by created_at desc
limit 20 offset 40
TIP
Clause order is strict: findfromwhereorder bylimitoffset

Full Combined Query

complete example
find [name, email, department, salary] from users
where status = "active" and salary > 50000
order by salary desc
limit 10
2
Section 2· +120 XP on complete

Expressions & Operators

RiverQL supports a rich set of operators for comparisons, logic, pattern matching, arithmetic, type casting, and time intervals.

Comparison Operators

OperatorMeaningExample
=Equalstatus = "active"
!=Not equalstatus != "banned"
<>Not equal (alt)status <> "inactive"
>Greater thanage > 21
>=Greater or equalsalary >= 75000
<Less thanprice < 100
<=Less or equaltotal <= 50.00
examples
find users where age > 21
find users where salary >= 75000
find products where price < 100

Logical Operators

Combine conditions with and, or, and not. Use parentheses to control precedence.

logical
find users where status = "active" and department = "Engineering"
find users where department = "Sales" or department = "Marketing"
find users where not status = "banned"
parentheses grouping
find users
where (department = "Sales" or department = "Marketing")
and salary > 60000

NULL Handling

null checks
find users where deleted_at is null
find users where email_verified_at is not null
null functions
-- coalesce: first non-null value
find [name, coalesce(nickname, name) as display_name] from users
 
-- nullif: returns NULL if values are equal (avoids division by zero)
find [name, nullif(discount, 0) as effective_discount] from products
 
-- ifnull: two-argument coalesce shorthand
find [name, ifnull(phone, "N/A") as contact] from users

BETWEEN / IN / NOT IN

between
find [name, created_at] from users
where created_at between "2024-01-01" and "2024-12-31"
 
find products where price between 10 and 50
in / not in
find users where status in ("active", "pending")
find users where department not in ("HR", "Legal")

Pattern Matching — LIKE / ILIKE

% matches any number of characters. _ matches exactly one character. ilike is case-insensitive.

pattern matching
find users where name like "%smith%"
find users where email like "%@gmail.com"
find users where name like "J_n"
find users where name ilike "%smith%" -- case-insensitive

Arithmetic & String Concat

arithmetic & concat
find [name, price * 1.1 as price_with_tax] from products
find [first || " " || last as full_name] from users
find [abs(amount), round(price, 2), ceil(score), floor(score)] from products

Type Casting

casting
-- Function style
find [name, cast(age as string) as age_str] from users
 
-- Shorthand operator ::
find [name, created_at::string as date_str] from users
NOTE
Supported types: string, integer, float, boolean, datetime, json

Interval Literals

Use shorthand suffixes for relative time calculations with now().

intervals
find users where created_at > now() - 30d
find users where last_login < now() - 90d
find users where created_at > now() - 1h
SuffixMeaning
yYears
monMonths
wWeeks
dDays
hHours
mMinutes
sSeconds

Named Parameters

named params
:start_date = "2024-01-01"
:end_date = "2024-12-31"
 
find [name, total, created_at] from orders
where created_at between :start_date and :end_date
3
Section 3· +150 XP on complete

Joins

Joins combine rows from two or more tables based on a related column. RiverQL supports all standard join types with concise, readable syntax.

Inner Join

Returns only rows that have matching values in both tables. join is shorthand for inner join.

inner join
find [u.name, o.total]
from users as u
join orders as o on u.id = o.user_id

Left Join

Returns all rows from the left table, with matched rows from the right (or NULL if no match).

left join
find [u.name, o.total]
from users as u
left join orders as o on u.id = o.user_id

Right Join

Returns all rows from the right table, with matched rows from the left.

right join
find [u.name, o.total]
from users as u
right join orders as o on u.id = o.user_id

Full Join

Returns all rows from both tables, with NULLs where there is no match on either side.

full join
find [u.name, o.total]
from users as u
full join orders as o on u.id = o.user_id

Cross Join

Returns the Cartesian product of both tables. No ON clause needed. Use with caution on large tables.

cross join
find [u.name, p.name as product]
from users as u
cross join products as p
limit 100
WARN
Cross joins on large tables can produce millions of rows. Always add limit.

Multiple Joins

Chain joins to combine three or more tables in a single query.

3-table join
find [u.name, o.total, p.name as product]
from users as u
join orders as o on u.id = o.user_id
join order_items as oi on o.id = oi.order_id
join products as p on oi.product_id = p.id

Self Join

Join a table to itself using different aliases — great for hierarchical data like org charts.

self join
find [e.name, m.name as manager]
from employees as e
left join employees as m on e.manager_id = m.id

Joins with Filters

join + filter + sort
find [u.name, o.total, o.status]
from users as u
join orders as o on u.id = o.user_id
where o.status = "paid" and o.total > 100
order by o.total desc
limit 20

Join Type Reference

TypeKeywordReturns
Innerjoin / inner joinMatching rows from both tables
Leftleft joinAll left rows + matched right (NULL if none)
Rightright joinAll right rows + matched left (NULL if none)
Fullfull joinAll rows from both, NULLs for no match
Crosscross joinCartesian product (no ON clause)
4
Section 4· +130 XP on complete

Aggregation

Aggregate functions compute a single value from a set of rows. Combine with group by to summarize groups, and having to filter them.

Aggregate Functions

FunctionDescription
count(*)Count all rows
count(expr)Count non-NULL values
count_distinct(expr)Count distinct non-NULL values
sum(expr)Sum of values
avg(expr)Average (mean)
min(expr)Minimum value
max(expr)Maximum value
single aggregates
find [count(*)] from users
find [sum(total)] from orders
find [avg(salary)] from employees
find [min(price), max(price)] from products

Multiple Aggregates

multiple aggregates
find [
count(*) as total_orders,
sum(total) as revenue,
avg(total) as avg_order,
min(total) as smallest,
max(total) as largest
]
from orders
where status = "paid"

GROUP BY

Group rows by one or more columns, then apply aggregates to each group.

single group
find [department, count(*) as headcount]
from employees
group by department
multi-column group
find [department, status, count(*) as cnt]
from employees
group by department, status

GROUP BY with Full Projection

full projection
find [
category,
count(*) as product_count,
avg(price) as avg_price,
min(price) as cheapest,
max(price) as most_expensive
]
from products
group by category
order by product_count desc

HAVING — Filter Groups

having filters groups after aggregation. Unlike where, it operates on aggregate values.

having
find [user_id, count(*) as order_count]
from orders
group by user_id
having count(*) > 5
TIP
where filters individual rows before grouping.
having filters groups after aggregation.

WHERE + HAVING Combined

where + having
find [department, avg(salary) as avg_sal]
from employees
where status = "active"
group by department
having avg(salary) > 75000

Aggregation with Joins

joins + aggregation
find [u.name, count(*) as order_count, sum(o.total) as total_spent]
from users as u
join orders as o on u.id = o.user_id
where o.status = "paid"
group by u.name
having total_spent > 1000
order by total_spent desc
limit 10
5
Section 5· +175 XP on complete

Window Functions

Window functions perform calculations across related rows without collapsing them. They preserve every row while computing values like rankings, running totals, and moving averages.

Window Syntax

syntax
function() over (partition by col order by col)
NOTE
partition by divides rows into groups (like GROUP BY, but rows are preserved).
order by defines row order within each partition.

ROW_NUMBER

Assigns a unique sequential integer to each row within its partition, ordered by the specified column.

row_number
find [
name,
department,
salary,
row_number() over (partition by department order by salary desc) as rank
]
from employees

RANK and DENSE_RANK

rank() leaves gaps after ties. dense_rank() does not.

rank vs dense_rank
-- rank(): ties share a rank, next rank has a gap (1, 1, 3...)
find [name, score, rank() over (order by score desc) as position]
from players
 
-- dense_rank(): no gaps (1, 1, 2...)
find [name, score, dense_rank() over (order by score desc) as position]
from players

LAG and LEAD

Access values from previous or next rows within the partition.

lag / lead
find [
date,
revenue,
lag(revenue, 1) over (order by date) as prev_day,
lead(revenue, 1) over (order by date) as next_day
]
from daily_stats
day-over-day
-- Day-over-day change
find [
date,
revenue,
revenue - lag(revenue, 1) over (order by date) as daily_change
]
from daily_stats

Running Totals

running total
find [
date,
amount,
sum(amount) over (order by date) as running_total
]
from transactions

Aggregates Over Windows

aggregate window
find [
name,
department,
salary,
avg(salary) over (partition by department) as dept_avg,
salary - avg(salary) over (partition by department) as diff_from_avg
]
from employees

Named Windows

Define a reusable window spec with window to avoid repetition.

named window
find [
name,
department,
salary,
avg(salary) over w as dept_avg,
max(salary) over w as dept_max,
min(salary) over w as dept_min
]
from employees
window w as (partition by department)

Top N Per Group

top-n per group
-- Top 3 highest-paid employees per department
find * from (
find [
name, department, salary,
row_number() over (partition by department order by salary desc) as rn
]
from employees
) as ranked
where rn <= 3

Window Function Reference

FunctionDescription
row_number()Unique sequential number per partition
rank()Rank with gaps after ties
dense_rank()Rank without gaps
lag(expr, N)Value from N rows before
lead(expr, N)Value from N rows after
first_value(expr)First value in the window frame
last_value(expr)Last value in the window frame
nth_value(expr, N)Nth value in the window frame
sum/avg/min/max over (...)Any aggregate used as a window function
6
Section 6· +200 XP on complete

Advanced Queries

CTEs, subqueries, set operations, CASE expressions, and cross-database joins — the full toolkit for complex data retrieval.

CTEs — Common Table Expressions

Define temporary named result sets with with. Makes complex queries readable and composable.

simple CTE
with active_users as (
find * from users where status = "active"
)
find [name, email] from active_users
chained CTEs
with
paid_orders as (
find * from orders where status = "paid"
),
user_totals as (
find [user_id, sum(total) as revenue]
from paid_orders
group by user_id
)
find [u.name, ut.revenue]
from users as u
join user_totals as ut on u.id = ut.user_id
where ut.revenue > 10
order by ut.revenue desc

Recursive CTEs

Traverse hierarchical data like org charts and category trees using with recursive.

recursive CTE
with recursive org_tree as (
-- Base case: top-level employees
find * from employees where manager_id is null
union all
-- Recursive: join children to parent
find [e.*]
from employees as e
join org_tree as t on e.manager_id = t.id
)
find * from org_tree

Subqueries

scalar subquery
-- Scalar subquery in WHERE
find [name, salary] from users
where salary > (
find [avg(salary)] from users
)
IN subquery
-- IN subquery
find [name, department] from employees
where department in (
find distinct [department] from departments
where budget > 100000
)
exists / not exists
-- EXISTS / NOT EXISTS
find [name] from users as u
where exists (
find [1] from orders as o where o.user_id = u.id
)
 
-- Find users with no orders
find [name] from users as u
where not exists (
find [1] from orders as o where o.user_id = u.id
)
derived table
-- Derived table (subquery in FROM)
find * from (
find [user_id, sum(total) as revenue]
from orders
group by user_id
) as user_revenue
where revenue > 500

Set Operations

set operations
-- UNION (deduplicates)
find [name, email] from customers
union
find [name, email] from suppliers
 
-- UNION ALL (keeps duplicates — faster)
find [name, email] from customers
union all
find [name, email] from suppliers
 
-- INTERSECT (rows in both)
find [user_id] from orders where year = 2024
intersect
find [user_id] from orders where year = 2025
 
-- EXCEPT (rows only in first)
find [user_id] from users
except
find [user_id] from orders where created_at > now() - 30d

CASE Expressions

searched case
-- Searched CASE
find [
name, salary,
case
when salary < 50000 then "Low"
when salary >= 50000 and salary < 100000 then "Medium"
when salary >= 100000 then "High"
else "Unknown"
end as salary_band
]
from users
simple case
-- Simple CASE
find [
name,
case status
when "active" then "Active User"
when "suspended" then "Suspended"
else "Unknown"
end as status_label
]
from users
case in order by
-- CASE in ORDER BY for custom sort priority
find [name, priority] from tasks
order by case priority
when "urgent" then 1
when "high" then 2
when "normal" then 3
else 4
end

Cross-Database Queries

Append @connection to any table name to query across different database systems.

cross-database join
-- Join PostgreSQL users with MySQL orders
find [u.name, o.total]
from users@pg as u
join orders@mysql as o on u.id = o.user_id
where o.status = "paid"
cross-database CTE
-- Cross-database CTE
with
pg_users as (
find [id, name] from users@pg
where status = "active"
),
mongo_logs as (
find [user_id, action, timestamp]
from logs@mongo
where timestamp > now() - 7d
)
find [pg_users.name, count(*) as login_count]
from pg_users
join mongo_logs on pg_users.id = mongo_logs.user_id
where mongo_logs.action = "login"
group by pg_users.name
order by login_count desc
limit 10
TIP
Push where filters before cross-database joins to minimize data transfer. Index join columns on both sides.
7
Section 7· +140 XP on complete

Data Modification

RiverQL maps SQL DML and DDL to human-readable commands: create for INSERT, update for UPDATE, remove for DELETE, and standard alter table syntax.

INSERT — create

create (INSERT)
-- Single row
create users { name: "Alice", email: "alice@example.com", age: 30 }
 
-- Multiple rows
create users [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
{ name: "Carol", email: "carol@example.com" }
]
 
-- Insert from query
create active_users_backup (
find * from users where status = "active"
)
 
-- Target a specific connection
create users@pg { name: "Dave", email: "dave@example.com" }

UPDATE

Modify existing rows with update ... set ... where. Always include where unless you intend to update all rows.

update
-- Basic update
update users
set status = "inactive", updated_at = now()
where last_login < now() - 90d
 
-- Update with expressions
update products
set price = price * 1.1
where category = "premium"
 
-- Target a specific connection
update users@pg
set status = "verified"
where email_verified_at is not null
WARN
Without where, the update applies to ALL rows in the table.

DELETE — remove

remove (DELETE)
-- Basic delete
remove users where status = "banned"
 
-- Delete with subquery
remove users
where id not in (
find distinct [user_id] from orders
where created_at > now() - 365d
)
 
-- Delete on a specific connection
remove logs@mongo
where timestamp < now() - 30d
WARN
Without where, all rows are deleted. Always use where unless you intend a full-table deletion.

CREATE TABLE

create table
-- Basic table
create table products (
name string,
price float,
category string default "general",
created_at datetime
)
 
-- With primary key and constraints
create table users (
id int primary key,
name string not null,
email string not null,
status string default "active"
)
 
-- Idempotent (if not exists)
create table if not exists cache (
key string primary key,
value json,
expires_at datetime
)

ALTER TABLE

alter table
alter table users add column bio string
alter table users add column tier string not null default "free"
alter table users drop column temp_data
alter table users alter column age type float
alter table users alter column status type string not null default "active"
alter table users alter column status drop default
alter table users rename column name to full_name
alter table users rename to customers
alter table users@pg add column notes string

Persisting Query Results — >>

Save results of any query to a table using the >> operator.

persist query results
-- Simple persist
find * from users >> user_backup
 
-- With conflict handling (upsert)
find [user_id, sum(total) as revenue]
from orders
group by user_id
>> user_revenue@pg
insert if exists on conflict replace
 
-- Ignore duplicates
find distinct [email] from new_signups
>> verified_emails
insert if exists on conflict ignore

Operations Reference

OperationRiverQLSQL Equivalent
Insert onecreate t { ... }INSERT INTO t VALUES (...)
Insert manycreate t [{ ... }, { ... }]INSERT INTO t VALUES (...), (...)
Insert from querycreate t (find ...)INSERT INTO t SELECT ...
Persist resultsfind ... >> targetCREATE TABLE AS + INSERT
Upsert>> target insert if exists on conflict replaceON CONFLICT DO UPDATE
Updateupdate t set ... where ...UPDATE t SET ... WHERE ...
Deleteremove t where ...DELETE FROM t WHERE ...
8
Section 8· +80 XP on complete

Meta Commands

Meta commands inspect database structure and query plans without fetching application data. Essential tools for exploration and optimization.

DESCRIBE

View the schema of any table — column names, types, constraints, and defaults.

describe
describe users
 
-- Target a specific connection
describe users@pg
describe orders@mysql
describe inventory.products@pg

SHOW TABLES

List all tables (or collections) in the current or specified connection.

show tables
show tables
 
-- For a specific connection
show tables @pg
show tables @mongo
show tables @mysql

EXPLAIN

View the execution plan without running the query. Essential before executing expensive operations.

explain
explain find [name] from users
where department = "Engineering"
order by salary desc
cross-db explain
-- Cross-database explain
explain find [u.name, o.total]
from users@pg as u
join orders@mysql as o on u.id = o.user_id
TIP
Always explain before running expensive cross-database joins to understand the fetch strategy.

Named Parameters

Define reusable session-scoped values with the : prefix. Persist for the duration of the session.

named params
:start_date = "2024-01-01"
:end_date = "2024-12-31"
:min_amount = 100
 
find [name, total, created_at] from orders
where created_at between :start_date and :end_date
and total > :min_amount

Comments

comments
-- This is a single-line comment
find users -- inline comment
 
/* This is a
multi-line comment */
find users where status = "active"

Multiple Statements

Separate multiple statements with semicolons to execute them in sequence.

multiple statements
:threshold = 1000;
find [name, total] from orders where total > :threshold;
describe orders

Connection Configuration (river.yaml)

river.yaml
- name: pg
kind: postgres
uri: "postgres://river:river@localhost:5432/river"
schema: public
 
- name: mysql
kind: mysql
uri: "mysql://river:river@localhost:3306/river"
schema: river
 
- name: mongo
kind: mongodb
uri: "mongodb://localhost:27017"
 
- name: sqlite
kind: sqlite
uri: "sqlite:river.db?mode=rwc"
NOTE
Avoid hyphens (-) in connection names. Use underscores instead.

The Cardinal Rules

RuleDescription
1. Every query starts with findfind is the universal SELECT keyword
2. Strict clause orderfind → from → where → group by → having → order by → limit → offset
3. Always use where with update/removeOmitting where affects all rows
4. @connection for cross-databaseAppend @name to route queries to specific databases
5. explain before expensive queriesUnderstand the plan before executing it

Your Progress

Mark sections complete to earn XP and unlock badges.

Achievements

0/8 unlocked
First Query
~
River Explorer
Join Master
#
Code Collector
Scholar
Grandmaster
Window Wizard
3-Day Streak

1

Level

0

Total XP

0d

Streak

Quick Reference

The essentials at a glance.

Clause Order

find [columns] from table[@conn]
where condition
group by columns
having condition
window name as (spec)
order by col [asc|desc]
limit N offset M

Cardinal Rules

  • 1.Every query starts with find
  • 2.Strict clause order — no exceptions
  • 3.Always use where with update/remove
  • 4.@connection enables cross-DB queries
  • 5.Use explain before expensive queries

Operators

=Equal
!= / <>Not equal
> >= < <=Comparison
+ - * / %Arithmetic
||String concat
::Type cast
>>Persist results
@Connection ref

Aggregate Functions

count(*)All rows
count(expr)Non-NULL values
count_distinct(expr)Unique values
sum(expr)Sum
avg(expr)Average
min(expr)Minimum
max(expr)Maximum

RiverQL — write once, query anywhere.

Built with v0