# iq
jq for NoSQL databases. Every page of https://zsltg.github.io/iq/ concatenated in
navigation order, from the Markdown sources under docs/docs/.
# Get started
docs/docs/index.md
# Get started
`iq` runs `jq`[^2] filters to query, dump, copy, diff and write data across NoSQL
databases, and their dump files, from a single static binary. See
[Drivers](drivers.md#drivers) for supported databases.
`iq` normalizes fetched values to JSON. The filter runs entirely client-side,
so one filter means the same thing everywhere.
The URI scheme[^4] chooses the backend. The filter is both the *transform* and
the *key selector*. The selector walks the parsed `jq` AST[^ast]. Based on the
AST, the selector executes a *bounded read*, a *streaming scan* (with
*pushdown*[^pushdown]), or a *materialized scan* (see
[How it works](how-it-works.md#how-it-works)).
Typed dumps carry native types across stores. As a result, a copy, a restore or a
migration is one command instead of an export plus a conversion script.
`iq` is inspired by [`sq`](https://sq.io "Command-line tool giving jq-style
access to SQL databases and files like CSV or Excel"), whose command set it
deliberately follows.
!!! note
`iq` is built with AI assistance, and every change passes the full test
suite, container-backed integration tests for every backend, and a mutation
gate before it lands (see
[CONTRIBUTING.md](https://github.com/zsltg/iq/blob/main/CONTRIBUTING.md)).
Queries are read-only. `--insert`, `--replace`, `iq data clear`,
`iq data drop` and `iq data delete` write to the target. `iq exec` forwards
a native command to the database, so it can write too. Use `--explain` to
see the [query plan](query-plan.md#query-plan) or `--dry-run` to report the
effect of a write, without changing anything. `iq exec` has no dry run.
Feedback and bug reports are very welcome. Report security problems privately,
see the [security policy](https://github.com/zsltg/iq/security/policy).
## Installation
`iq` ships as a single static binary (no runtime dependencies, no CGO[^5]).
=== ":fontawesome-brands-linux: Linux"
```sh
curl -fsSL https://raw.githubusercontent.com/zsltg/iq/main/install.sh | sh
```
!!! note "Version & Location"
The script downloads the release for your OS/arch, verifies its SHA-256[^6]
against the release checksums, and installs the binary. `IQ_VERSION`
pins a version and `IQ_INSTALL_DIR` picks the target directory.
You can also download a `.deb`, `.rpm`, `.apk`, or Arch `.pkg.tar.zst` from the
[releases](https://github.com/zsltg/iq/releases).
=== ":fontawesome-brands-apple: macOS"
```sh
brew install zsltg/tap/iq
```
The Linux install script above works on macOS too.
=== ":fontawesome-brands-windows: Windows"
```powershell
scoop bucket add zsltg https://github.com/zsltg/scoop-bucket
scoop install iq
```
=== ":fontawesome-brands-golang: Go"
```sh
go install github.com/zsltg/iq@latest
```
### Verify a release
Every release signs `checksums.txt` with a keyless [cosign](https://docs.sigstore.dev/)
signature from the release workflow, and carries SLSA build provenance for every
artifact (`multiple.intoto.jsonl`). The install script checks the SHA-256 of the
archive it downloads. To check a download yourself, first make sure that
`checksums.txt` comes from the iq release workflow:
```sh
cosign verify-blob checksums.txt \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/zsltg/iq/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum --ignore-missing -c checksums.txt
```
To check the build provenance of an archive, use
[slsa-verifier](https://github.com/slsa-framework/slsa-verifier):
```sh
slsa-verifier verify-artifact iq_0.37.1_linux_amd64.tar.gz \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/zsltg/iq --source-tag v0.37.1
```
Releases before v0.37.1 carry `checksums.txt` only, with no signature or
provenance.
## Building from source
```sh
git clone https://github.com/zsltg/iq
cd iq && make build
```
## The basics
Register a source for each store you work with. Then make one of them active:
```sh
iq add -n orders 'mongodb://localhost:27017/shop?collection=orders'
iq add -n staging 'mongodb://staging:27017/shop?collection=orders'
iq add -n cache redis://localhost:6379/0
iq add -n snap file:///backups/prod.rdb
iq src orders
```
```sh { title='Check the list of sources you added' }
iq ls
```
```sh { title='Inspect the active source' }
iq inspect
```
```sh { title='Explain the query plan for a bounded read, a dry run' }
iq '.["o-42"]' --explain -v
```
```sh { title='Run the query to get the document with id "o-42"' }
iq '.["o-42"]'
```
```sh { title='Run a query to get all documents in batches, a streaming scan' }
iq '.[]'
```
```sh { title='Stream a filtered sample, pushed to the server where it can be' }
iq --src orders '.[] | select(.status == "new") | {id, total}'
```
```sh { title='Query a backup without restoring it' }
iq --src snap '.[] | select(.active)'
```
```sh { title='Copy one store into another, native types intact' }
iq --src orders --insert cache
```
```sh { title='Diff two environments, data or inferred schema' }
iq diff orders staging --schema
```
You can find detailed examples in [Sources](sources.md#sources), [Query data](query-data.md#query-data) and [Write data](write-data.md#write-data).
For more advanced usage check [Output](output.md#output), [Query plan](query-plan.md#query-plan), [Cookbook](cookbook.md#cookbook) and
[Loading exports](loading-exports.md#loading-exports).
For debugging, see [Diagnostics & Logging](diagnostics-and-logging.md#diagnostics-logging).
Supported data sources are listed in [Drivers](drivers.md#drivers).
## Shell completions
The `.deb`, `.rpm`, `.apk` and `.pkg.tar.zst` packages install
[Bash](https://tiswww.case.edu/php/chet/bash/bashtop.html "GNU Bourne-Again
SHell, the default shell on most Linux distributions"),
[Zsh](https://www.zsh.org/ "Extended Bourne shell, the default on macOS since
Catalina") and [fish](https://fishshell.com/ "Friendly Interactive SHell,
deliberately non-POSIX, with autosuggestions built in") completions for you.
For a [brew](https://brew.sh/ "Homebrew, the third-party package manager for
macOS and Linux"), [scoop](https://scoop.sh/ "Command-line installer for
Windows, installing per-user without admin rights"),
[go-install](https://go.dev/ref/mod#go-install "Builds and installs a Go
command from its module path into GOPATH/bin") or source build,
`iq completion ` prints a script to install by hand.
=== ":simple-gnubash: Bash"
```sh title="load in the current session"
eval "$(iq completion bash)"
```
```sh title="copy it to the completion path"
iq completion bash | sudo tee /usr/share/bash-completion/completions/iq >/dev/null
```
=== ":simple-zsh: Zsh"
```sh title="write to a directory on your $fpath, then restart the shell"
iq completion zsh > ~/.zsh/completions/_iq
```
=== ":simple-fishshell: fish"
```sh title="write to a directory on your $fish_complete_path"
iq completion fish > ~/.config/fish/completions/iq.fish
```
=== ":material-powershell: PowerShell"
```sh title="append to your profile"
iq completion powershell >> $PROFILE
```
Completions cover the commands, their sub-subcommands and flags. They also cover
the saved source handles, groups, and config-option keys, which they read live
from your config. As a result, `iq --src ` offers the sources `iq ls` lists.
A flag that takes a closed
set offers that option's own values.
`iq inspect --only ` and `iq diff --section ` offer the
introspection subcommands of the selected source's backend, worked out from its
saved URI.
The `jq` filter itself is a program, not a completable value. As a result, `iq`
offers no candidates there (and never falls back to filenames). `iq` also offers
no candidates for the backend verb of `iq exec` and its operands.
Every completion is offline. It reads your config file and nothing else. As a
result, a `` never opens a connection, never reads the OS
keyring[^keyring], and cannot hang. That is why a collection suffix does not
complete. `iq --src shop.` offers nothing, because listing collections
needs a connection.
## Man page
The `.deb`, `.rpm`, `.apk`, and `.pkg.tar.zst` packages also install an `iq(1)` manual page, so `man iq` works
after a package install. For any other install, pipe it into your man path:
```sh
iq man | sudo tee /usr/share/man/man1/iq.1 >/dev/null
```
[^2]: `jq` is a widely-used command-line utility and very high-level, functional, domain-specific programming language designed for processing JSON data. https://jqlang.org
[^4]: RFC3986 proposes a generic URI syntax and a process for resolving URI references that might be in relative form, along with guidelines and security considerations for the use of URIs on the Internet. https://datatracker.ietf.org/doc/html/rfc3986
[^5]: Cgo enables the creation of Go packages that call C code. https://pkg.go.dev/cmd/cgo
[^6]: SHA-256 is a Secure Hash Algorithm with a message digest size of 256. https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.180-4.pdf
[^ast]: An abstract syntax tree is the tree that a parser builds from the source of a program. Here, it is the parsed `jq` filter that the key selector inspects to decide how to read (see [How it works](how-it-works.md#read-strategies)). https://en.wikipedia.org/wiki/Abstract_syntax_tree
[^pushdown]: Predicate pushdown hands part of the filter to the database, so that the database returns only matching items instead of everything for client-side filtering. Each driver page lists what the driver can push (see [Drivers](drivers.md#drivers)). https://en.wikipedia.org/wiki/Predicate_pushdown
[^keyring]: The operating system's credential store (macOS Keychain, Windows Credential Manager, the Secret Service on Linux), where `--store keyring` sources keep their secrets (see [Configuration](configuration.md#keyring-keyring)). https://pkg.go.dev/github.com/zalando/go-keyring
# How it works
docs/docs/how-it-works.md
# How it works
A Go command-line tool that runs [jq](https://jqlang.github.io/jq/) filters
against NoSQL databases. The URI scheme chooses the backend. The query
core is driver-agnostic, so further backends slot in behind the same port.
The filter is both the transform and the key selector. Its top-level paths name the keys to
fetch, so a normal query reads a bounded set of keys. A `.[]`-rooted filter streams the
keyspace in pages. A filter that collapses the keyspace into one value materializes only behind
`--unbounded`. `iq` normalizes fetched values to JSON. The filter then runs entirely
client-side, so its semantics are identical for every backend.
The CLI and `iq mcp` are two thin delivery mechanisms over that one core. The MCP server
exposes the CLI's own operations as tools, resolves the same saved sources, and runs the same
engine. As a result, it adds no port and changes no classification. It adds only its own bounds,
a tool set fixed at startup by `--allow`, per-result item and byte caps, and the CLI's redacted
error shape.
`iq` is inspired by [sq](https://github.com/neilotoole/sq). Much of its command
set (the `.` addressing along with many subcommands and
flags) deliberately follows sq's to make the tool feel familiar.
The `jq` semantics are identical for any future backend.
[gojq](https://github.com/itchyny/gojq) (pure Go, no CGO) provides `jq`, keeps `iq` a
single static binary, and exposes the AST the key selector walks.
## Read strategies
The shape of the filter decides how much `iq` reads. Every filter takes one of
three routes:
1. **Bounded reads** (with explicit keys) only read the specified subset of
items from the source. The keys you asked for bound the cost, never
the size of the database.
2. **Streaming scans** (a filter rooted at `.[]`, for example `.[]`, `.[] | select()`,
`.[].title`) process each value independently. They walk the keyspace
in pages, run the filter page by page, and emit as they go. Memory stays
constant and results appear progressively.
3. **Materialized scans** (a filter that collapses the collection into one
value, `.`, `keys`, `length`, `map()`, `group_by`, `sort_by`, aggregates)
read all items into memory in batches before applying the filter.
```mermaid
graph LR
Q1[".[#quot;1#quot;]"] -->|names a key| T1["bounded read"] --> R1["{ #quot;title#quot;: #quot;The Go…#quot; } one value"]
Q2[".[]"] -->|iterates values| T2["streaming scan"] --> R2["{ … } then { … } then … each value, streamed"]
Q3["."] -->|whole root| T3["materialized scan (needs --unbounded)"] --> R3["{ #quot;1#quot;: {…}, #quot;2#quot;: {…} } one object, every key"]
classDef bounded fill:#e6f4ea,stroke:#137333,color:#0b3d1f;
classDef streaming fill:#fef7e0,stroke:#8a5a00,color:#5c3d00;
classDef materialized fill:#fce8e6,stroke:#c5221f,color:#5c0f0a;
class T1 bounded;
class T2 streaming;
class T3 materialized;
```
On a streaming scan, **pushdown** compiles what it can of the filter's
`select()` into a backend-neutral predicate. Pushdown then hands the predicate
to the driver. This shrinks how much data is transferred or decoded.
The predicate is deliberately weaker than the filter, so the engine re-runs the
full filter per page to drop the extra matches it admits.
!!! tip "Check the strategy of a query"
Before you execute a query, use `--explain` to see which strategy `iq`
will use for it.
!!! note "Cost"
A *bounded filter* runs client-side over only the named keys, so its cost is
`O(keys requested)`.
A *streamable scan* runs in `O(page)` memory.
!!! note "Unbounded queries"
`--unbounded` means "permit loading the whole dataset into memory".
The flag names the cost property (loading everything), not any one store's
mechanism, so it will mean the same thing for all backends.
You can also pass it on a streaming filter. It then switches that filter
from batched streaming to a single materialized pass. The result is
key-sorted output and a consistent snapshot instead of scan order.
!!! note "Streaming data"
Streamed output is *best-effort*. Values arrive in scan order (not
key-sorted). If the keyspace is resized mid-scan, an element can repeat.
This is the price of never holding more than one page.
If you need sorted, exactly-once output, use `--unbounded`.
!!! note "Progress"
A scan has no reliable upfront total (for example Redis `SCAN`, Mongo
cursor). For this reason, `iq` shows an animated spinner with a running
`N scanned` count on *stderr*. As a result, a sparse `.[] | select()` over
a large keyspace is never silent.
When a backend can supply a cheap approximate total (for example MongoDB's
`estimatedDocumentCount` for an unfiltered whole-collection scan, Redis's
`DBSIZE` for its whole-keyspace `MATCH *` scan), `iq` shows the count against
that total as `N scanned (~M est)`.
The tilde marks the estimate as a hint. The estimate comes from cached
metadata and drifts under concurrent writes. As a result, the scan can
exceed the estimate, and the estimate never becomes a percentage bar.
`iq` shows no total for a pushed-down filtered scan (it walks a subset) or
for a cross-source scan (a per-source estimate misleads the
aggregate).
## Query routes
The **selector** classifies a jq filter. `iq` optionally **decomposes** a scan
into a native predicate. Each backend then maps that predicate its own way.
Regardless of pushdown, the full jq re-runs client-side. As a result, the pushed
predicate is only ever a conservative pre-filter, and results are identical with
or without it.
```mermaid
graph TD
F["jq filter (CLI)"] --> SEL["selector.Keys (AST analysis)"]
SEL -->|bounded| GET["KVStore.Get(keys)"]
SEL -->|"streamable scan"| CMP{"FilteredScanner? (pushdown on)"}
SEL -->|"holistic scan"| MAT["materialize (--unbounded)"]
CMP -->|yes| PD["pushdown.Compile → predicate.Node (adapter pre-filters server-side)"]
CMP -->|no| RS["KVStore.ScanBatches (full scan, no pushdown)"]
GET --> JQ["run full jq client-side, per batch"]
MAT --> JQ
PD --> JQ
RS --> JQ
JQ --> OUT["format renderer → output"]
```
A scan emits per-page progress to a stderr spinner (CLI only, off unless
attached to a terminal). An unfiltered scan can also fetch a cheap up-front total
estimate (where the backend metadata makes it possible).
## Write routes
Writes ride the query command. There is no separate copy tool. Items arrive
from `--src` (a live source or a `file://` dump) or piped stdin. `iq` reads
each item as a typed record, so the native type survives the trip.
The `jq` filter transforms each item with its key preserved. This transform is
the one place where a filter runs per item rather than over the whole keyspace.
Iteration is implicit, and you do not write `.[]`.
```mermaid
graph TD
MV["iq --insert / --typed (CLI)"] --> MSRC["source: --src (live or file:// dump) / stdin → TypedScan"]
MSRC --> TX["per-item jq transform + re-key"]
TX --> DST{"--insert or --typed?"}
DST -->|--insert| PUT["Putter.Put (upsert / insert-only)"]
DST -->|--typed| ENC["emit {key,type,value} → jsonl / json / jsona / yaml"]
PUT --> BW["backend adapter: type-aware native writes"]
LF["iq data clear / drop / delete (CLI)"] --> CAP["Clearer.Clear / Dropper.Drop / Deleter.Delete (capability-gated)"]
```
The transformed stream then takes one of two exits.
- **`--insert `** hands each record to the destination's `Putter`.
Existing keys are overwritten (upsert) unless `--no-overwrite` makes the run
insert-only. `--replace` empties the destination first (with
confirmation, or `--force`). The backend adapter translates each typed value
into its native write, so a Redis hash lands as a hash again, not as a JSON
blob.
- **`--typed`** skips the store and emits the same records as
`{"key", "type":…, "value":…}` envelopes in the chosen format (`--jsonl`
by default). The dump re-imports through a `file://` source or a piped
`--insert`, which closes the loop back into the diagram's source node.
Destructive operations are a separate entry point, not a filter outcome. The
`iq data clear / drop / delete` subcommands call their own capability-gated
ports. As a result, a backend that has no native drop refuses rather than
emulating one. For the same reason, no query ever deletes as a side effect.
See the full flag tables and key-mapping rules in
[Write data](write-data.md).
## Null vs. Missing
Modern query standards treat an *absent* field and an explicit *null* as
distinct values.
[PartiQL](https://partiql.org/) has both `NULL` and `MISSING`, and `MISSING`
drops out of a projection where `NULL` is carried through.
[SQL++](https://arxiv.org/abs/1405.3631) makes missing a value of its own and
documents real divergence. The same path returns `null` in AsterixDB, `missing`
in Couchbase, and an error in SQL.
[RFC 9535](https://www.rfc-editor.org/rfc/rfc9535) (JSONPath) models absence as
`Nothing`, again distinct from `null`. `iq` takes a deliberate, layered
position in that vocabulary rather than one blanket rule.
- **The jq layer reads missing as `null`.** gojq evaluates entirely
client-side. As a result, `.a` on a document without `a` yields `null`, the
same on every backend. This is the uniform semantics that the whole tool
promises. A filter behaves identically whether the field is absent, stored as
`null`, or the source has no such field at all.
- **The schema layer preserves the distinction.** `iq schema` tracks
*parent-relative presence*. A field observed on some documents but not others
is optional, separate from a field that is present-and-nullable. As a result,
the inferred shape measures logical structure, not the jq layer's collapse.
- **Drivers decline pushes whose backend semantics diverge.** A conjunct
is pushed only when the backend reproduces `jq`'s answer for every input
including missing and null. Elasticsearch `== null` (which no single term
matches as absent-or-null) is declined and re-filtered client-side rather
than pushed with the wrong meaning. The per-driver push/not-push tables
record each call.
So the collapse is a query-layer convenience, not a loss. `iq` keeps the
distinction where it carries information (schema inference, pushdown safety).
It hides the distinction where uniformity matters more (the query layer).
# Sources
docs/docs/sources.md
# Sources
`iq` connects only through **saved sources**, a named connection you register
once, then select by name or as the default.
!!! note "Configuration"
Sources live in a TOML file at `/iq/iq.toml` (for example
`~/.config/iq/iq.toml`), written `0600` because a URI can carry a password.
Override the path with `IQ_CONFIG`, or per run with the global
`--config ` flag (which wins over `IQ_CONFIG`).
A source added with `--store keyring` keeps no password in this file. The
password lives in the OS keyring (Secret Service on Linux, Keychain on
macOS, Credential Manager on Windows). `iq` splices it back into the URI
only when connecting.
## Add `add`
`iq add [flags]`
Register a source from a connection URI. The URI is the only positional
argument.
Each driver supports a different set of URI parameters (see
[Drivers](drivers.md)).
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| `-a` | `--active` | ✗ | make the new source the active source |
| `-d ` | `--driver ` | auto-detect | expected backend driver. It must match the URI scheme |
| `-n ` | `--handle ` | the keyspace the URI names | handle for the source, derived from the URI when omitted |
| `-p` | `--password` | ✗ | prompt for the URI password or read it from stdin |
| | `--skip-verify` | ✗ | skip the post-add reachability check |
| | `--store ` | `inline` | where the URI's password is kept, `inline` (in the config file) or `keyring` (OS keyring) |
```sh { title='Add an inactive MongoDB source, defaults to handle "books"' }
iq add mongodb://localhost:27017/books
```
```sh { title='Add an inactive Redis source, sets the handle to "cache"' }
iq add -n cache redis://localhost:6379/0
```
```sh { title='Add a Cassandra source and make it active, defaults to handle "orders"' }
iq add -a 'cassandra://localhost:9042/shop?table=orders'
```
```sh { title='Add an inactive MongoDB source, prompts for the password for a user named "iq" in the "iq" database, defaults to handle "books"' }
iq add -p 'mongodb://iq@localhost:27018/iq?collection=books'
```
```sh { title='Add an inactive MongoDB source, prompts for the password for a user named "root" in the "admin" database, defaults to handle "books"' }
iq add -p 'mongodb://root@localhost:27018/iq?authSource=admin&collection=books'
```
!!! tip "Escaping the source URI"
If the URI contains `?` (and `&`), your shell can interpret it as a
wildcard, operator or separator. As a result, you must escape it (for
example `iq add 'mongodb://localhost:27017/iq?collection=books'`).
!!! tip ""add" shadowing"
`iq add` shadows `jq`'s built-in `add` filter at the top level. To sum with
`jq`, write it inside a larger expression, for example `iq '[ .a, .b ] | add'`.
## Group `group`
`iq group [name] [flags]`
Show, set, or clear the active group.
A `/` in a name groups sources (`prod/books`, `dev/books`). Set an active group with
`iq group prod`. An unqualified name then resolves inside it. For example, `iq src books` selects
`prod/books`. If the group has none, `iq src books` falls back to a top-level `books`.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--clear` | ✗ | clear the active group |
```sh { title='Show the active group' }
iq group
```
```sh { title='Set the active group to "dev"' }
iq group dev
```
```sh { title='Clear the active group' }
iq group --clear
```
!!! tip "Listing groups"
You can list the already created groups with `iq ls -g`.
## List `ls`
`iq ls [group] [flags]`
List saved sources, the active one marked with `*`.
An optional `[group]` limits the listing to sources in that group.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| `-v` | `--verbose` | ✗ | show a header and a `FORMAT` (a file source's detected dump format) and an `OPTIONS` (the source's stored option defaults) column :material-earth:{ title="Global flag" } |
| `-g` | `--group` | ✗ | lists groups instead of sources |
| `-j` | `--json` | ✗ | emit machine-readable JSON output |
| `-y` | `--yaml` | ✗ | emit machine-readable YAML output |
| | `--reveal` | ✗ | prints a password stored inline in the config verbatim |
| | `--expand` | ✗ | resolves a keyring-backed source's stored password and inlines it (combine with `--reveal` to print verbatim) |
```sh { title='List saved sources' }
iq ls
```
```sh { title='List groups' }
iq ls -g
```
```sh { title='List saved sources, the saved options (if there is any) and the format for file sources' }
iq ls -v
```
```sh { title='List saved sources, prints passwords verbatim' }
iq ls --reveal
```
## Move `mv`
`iq mv [flags]`
Rename a source to a new full handle, or move it into a group by giving a
group-qualified target (`iq mv books prod/books`).
When `` names a group, every source under it is re-prefixed
(`iq mv prod staging` renames `prod/*` to `staging/*`).
The active source and group follow the move. A keyring-backed source's stored
credential moves with it.
```sh { title='Rename source named "shop" to "catalog"' }
iq mv shop catalog
```
```sh { title='Move source named "books" into the group "prod"' }
iq mv books prod/books
```
```sh { title='Move every source in the group named "prod" to the group named "staging"' }
iq mv prod staging
```
## Ping `ping`
`iq ping [name...] [flags]`
Open each source and round-trip a cheap command (for example Redis `PING` or
MongoDB `{ping:1}`), reporting its driver and the round-trip time, or the error.
With no arguments, `iq ping` pings the active source. Otherwise, each argument
is a source handle or a group (pinging every member). `--all` pings every saved
source and takes no arguments.
`--timeout` bounds each check. Exits with a non-zero exit code if any
source is unreachable.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--all` | ✗ | ping every saved source, rejects source arguments |
| | `--timeout` | `5s` | per-query timeout :material-earth:{ title="Global flag" } |
```sh { title='Ping the active source' }
iq ping
```
```sh { title='Ping the sources named "cache" and "shop"' }
iq ping cache shop
```
```sh { title='Ping the sources in the group named "dev"' }
iq ping dev
```
```sh { title='Ping every saved source' }
iq ping --all
```
## Remove `rm`
`iq rm ... [flags]`
Remove saved sources or whole groups. Each argument is a source handle or a
group name (which removes every source under it).
The removal is atomic. If any argument names neither a source nor a group,
nothing is removed. A keyring-backed source's stored credential is deleted too.
```sh { title='Remove a single source named "cache"' }
iq rm cache
```
```sh { title='Remove multiple sources named "cache", "shop" and "dev/books"' }
iq rm cache shop dev/books
```
```sh { title='Remove all sources in the group named "dev"' }
iq rm dev
```
## Show/Set `src`
`iq src [name] [flags]`
Show or set the active source.
`--src` :material-earth:{ title="Global flag" } is global. Every command accepts it (see
[Global flags](global-flags.md#global-flags)).
Once a source is active, every query runs against it. Select a different source for a single
command with `--src`/`-s`, without changing the active one. Address a MongoDB collection or a
Cassandra table with a dotted `handle.collection` / `handle.table` suffix. With no active source
and no `--src`, the command errors. There is no ambient URI or environment fallback.
```sh { title='Show the active source' }
iq src
```
```sh { title='Set the active source to "shop"' }
iq src shop
```
!!! tip "Per command source selection"
You can run commands against a specific source which can be different from
the active one, for example `iq --src books '.["2"]'`.
## Inspect `inspect`
`iq inspect [source] [flags]`
Show a source's native server/database introspection.
The positional argument names the source, like `iq inspect books`. With none, it
uses --src or the active source.
Certain sources accept `sq`-style addressing (for example MongoDB
`.` or Cassandra `.`) to pick the
keyspace, overriding the default in the source's URI (for example
`?collection=` or `?table=`).
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--list` | ✗ | list the subcommands/sections available for the source |
| | `--only ` | ✗ | narrow to these sections or subcommands |
| | `--reveal` | ✗ | print an inline-stored password verbatim in the location header instead of redacting it |
| | `--expand` | ✗ | resolves a keyring-backed source's stored password and inlines it (combine with `--reveal` to print verbatim) |
| `-j` | `--json` | ✗ | emit machine-readable JSON |
| `-y` | `--yaml` | ✗ | emit machine-readable YAML |
```sh { title='Inspect the active source on the database level' }
iq inspect
```
```sh { title='Inspect a named source on the database level' }
iq inspect shop
```
```sh { title='Inspect a named source on the collection level, output in JSON' }
iq inspect shop.orders -j
```
```sh { title='Inspect a named source on the database level, narrow the sections to "dbStats" and "serverStatus"' }
iq inspect shop --only dbStats,serverStatus
```
## Diff `diff`
`iq diff [=] [=] [flags]`
Compare two saved sources across the layers a schemaless store can meaningfully
compare. Selecting no layer defaults to `--data`. Layers combine.
Each side can carry a `jq` filter, so a diff can be scoped to part of a
keyspace. Give it per side as `=`, or for both sides at once with
`--filter`. A side's own filter takes precedence.
Exits non-zero when the sources differ and zero when they match
(diff(1)-style[^2]), so scripts can branch on the exit status. Bounded by
`--timeout`.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--data` | ✓ | diff items key by key (cross-driver allowed, for example MongoDB `_id` and Redis key) |
| | `--filter ` | none | jq filter you root at `.[]`, scoping both sides. A spec's own `source=` overrides it for that side |
| | `--patch` | ✗ | emit an RFC 6902 JSON Patch[^1] that transforms the left source into the right (single layer only). For `--data`, the pointers read `//` over the whole keyspace map. `--patch` excludes `--json`/`--yaml`/`--set-arrays` |
| | `--schema` | ✗ | diff an inferred field/type shape (cross-driver allowed) based on a sample (use `--sample` to change the sample size) |
| | `--sample ` | `1000` | max items sampled per side for --schema (0 = all) |
| | `--stats` | ✗ | diff native introspection trees (same driver only). Use with `--section` to narrow it down |
| | `--section ` | full set | introspection section(s) for `--stats` (comma-separated or repeatable) |
| | `--set-arrays` | ✗ | compare arrays order-insensitively as multisets (duplicates counted). Membership deltas are reported at the array's own path with no index segment. A pure reorder becomes no difference |
| `-j` | `--json` | ✗ | emit machine-readable delta in JSON |
| `-y` | `--yaml` | ✗ | emit machine-readable delta in YAML |
```sh { title='Diff the "prod" and "staging" source with a filter that applies to both sides' }
iq diff prod staging --filter '.[] | select(.status == "new")'
```
```sh { title='Diff the "prod" and "staging" source with a per-side filter' }
iq diff 'prod=.[] | select(.type == "order")' 'staging=.[] | select(.kind == "ORDER")'
```
```sh { title='Diff a single key from the "prod" and "staging" source' }
iq diff 'prod=.["orders:42"]' 'staging=.["orders:42"]'
```
!!! tip "Filtering"
Write the filter `.[]`-rooted, as on a plain query (iteration is not
implicit here, unlike `--insert`). There are two reasons. A keyed diff
needs each item to keep its key. A pushable `select(...)` narrows the read
at the backend rather than merely narrowing the report.
`iq diff prod staging --filter '.[] | select(.status == "new")'`
A filter can instead name a single key (`prod=.["orders:42"]`) to compare
one document. An absent key then reports as removed rather than as a
change to null.
A filter that collapses the keyspace (`keys`, `map(...)`) or fans one item
out into several values (`.[] | .tags[]`) is refused. Neither leaves a key
to match on.
!!! warning "Data diff"
`--data` without filtering reads both keyspaces fully into memory. As a
result, it costs memory proportional to the two sources. This is a
deliberate tradeoff, because an added/removed diff needs both key sets at
once.
A filter narrows that read, and a pushable one narrows it at the backend.
Cross driver can be useful for verifying a migration. But the identity
match is only as meaningful as the keys lining up. It is a power-user tool,
not a schema comparison.
!!! warning "Schema diff"
`--schema` used across drivers gives each field path (with `[]`
array-element and `{}` map-value wildcards) a canonical type
(`integer`/`number`/`string(fmt)`/`map`/`array`) and a `required`/`optional`
presence. As a result, it measures logical shape rather than sampling luck
and stays quiet under resampling.
The shape is sampled (with size set with `--sample`) and inferred, never
declared, so a wider sample yields a truer shape.
Two backends that genuinely normalize a native type differently (a
timestamp as an RFC3339[^3] string vs an epoch number) still diff. That is
the JSON each serves back. The format tags make the row legible rather
than mysterious.
Arrays are aligned by a longest common subsequence, so a single insertion
reports one addition rather than a cascade at every later index.
## Schema `schema`
`iq schema [source[=]] [flags]`
Sample a source and project a schema inferred from its values (the same
inference [`diff --schema`](#diff-diff) uses).
Unlike [`inspect`](#inspect-inspect), which shows a backend's native
introspection, `schema` infers a driver-agnostic shape, never declared. The
field/type structure is sampled (with a sample size defined with `--sample`)
from the values themselves. As a result, a wider sample yields a truer shape.
It describes values, not keys. A non-object keyspace is legal (a string
keyspace emits `{"type":"string"}`). A filter that fans one item out into
several values is fine here, even though a keyed [`diff`](#diff-diff) must
refuse it.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--filter ` | none | jq filter scoping which items the shape is inferred from. The spec form `source=` sets it per source |
| | `--format ` | `jsonschema` | picks the contract dialect the shape projects into: JSON Schema draft 2020-12[^4] (`jsonschema`) or Open Data Contract Standard v3.1.0[^5] (`odcs`) |
| | `--sample ` | `1000` | max items sampled (0 = all) |
| `-y` | `--yaml` | ✗ | emit YAML instead of JSON (`jsonschema` only, `odcs` is always YAML) |
```sh { title='Schema of the active source' }
iq schema
```
```sh { title='Schema of the keyspace "orders" in the source "shop"' }
iq schema shop.orders
```
```sh { title='Schema of the source "dev" using a larger sample size' }
iq schema dev --sample 5000
```
```sh { title='Schema of a filtered subset in the keyspace "orders" in the source "dev"' }
iq schema 'dev.orders=.[] | select(.active)'
```
```sh { title='Save schema into a file from keyspace "orders" from the source "dev"' }
iq schema dev.orders > orders.schema.json
```
```sh { title='Save schema into a file from keyspace "orders" from the source "dev" in ODCS format' }
iq schema dev.orders --format odcs > orders.odcs.yaml # emit an ODCS v3.1.0 contract
```
!!! tip "Scoped schema"
A `source=` spec (or `--filter`) scopes which items the shape is
inferred from. For example, `iq schema 'prod=.[] | select(.active)'`
describes only the active ones. The sample cap then applies to the
survivors, so a selective filter walks further into the keyspace to fill
it.
!!! note "JSON Schema"
JSON Schema draft 2020-12 (`jsonschema`, the default) can be used for
interop with code generators, like
[`quicktype`](https://github.com/glideapps/quicktype).
`iq schema prod.orders > orders.schema.json && quicktype -s schema
orders.schema.json -l go`
!!! note "Open Data Contract Standard"
Open Data Contract Standard v3.1.0 (`odcs`) is a YAML data contract for
tools like
[`datacontract-cli`](https://github.com/datacontract/datacontract-cli),
[Soda](https://github.com/sodadata/soda-core)
and [Great Expectations](https://greatexpectations.io/).
It carries nine logical types with no binary or decimal member. As a
result, `iq`'s date-time and date formats demote to `logicalType: date`
with a JDK format pattern. A UUID stays `string` with the `uuid` format.
Base64-binary and exact-decimal values stay plain `string`.
The contract's identifiers (`id`, `name`, schema-object name) derive
deterministically from the source handle and keyspace. They use no
timestamps or random ids.
[^1]: JSON Patch defines a JSON document structure for expressing a sequence of
operations to apply to a JavaScript Object Notation (JSON) document. It is
suitable for use with the HTTP PATCH method. The "application/json-patch+json"
media type is used to identify such patch documents.
https://datatracker.ietf.org/doc/html/rfc6902
[^2]: `diff(1)` is a classic Unix command-line utility that compares two files
(or directories) line by line and outputs the differences between them.
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/diff.html
[^3]: Date and time format for use in Internet protocols that is a profile of
the ISO 8601 standard for representation of dates and times using the Gregorian
calendar https://datatracker.ietf.org/doc/html/rfc3339
[^4]: JSON Schema is a declarative language for defining structure and
constraints for JSON data. https://json-schema.org/draft/2020-12
[^5]: The Open Data Contract Standard (ODCS) is an open-source, vendor-neutral specification (maintained under the Linux Foundation's LF AI & Data) for defining data contracts in machine-readable format (YAML or JSON). https://bitol-io.github.io/open-data-contract-standard
# Query data
docs/docs/query-data.md
# Query data
The default action, a `jq` filter run against the active source (see
[Sources](sources.md)).
The top-level paths name the keys to fetch. The result is printed as pretty
JSON by default (see [Output formats](output.md))
```sh { title='Fetch the key "greeting"' }
iq '.greeting'
```
```sh { title='Fetch the key "book:1" (a key containing a colon needs bracket-quoting)' }
iq '.["book:1"]'
```
```sh { title='Fetch the key "book:1" and extract one field' }
iq '.["book:1"].title'
```
```sh { title='Fetch keys "book:1", "book:2" and project a field from each' }
iq '[ .["book:1"].title, .["book:2"].title ]'
```
```sh { title='Values are strings, convert before arithmetic calculations' }
iq '.["book:2"].price | tonumber + 5'
```
!!! warning "Escaping"
Always wrap the filter in single quotes. `jq` syntax is full of characters
that the shell otherwise expands or splits: brackets (`[ ]`), whitespace,
`|`, `*`, `$`. Bracket-quoting a colon key like `.["book:1"]` reads as a
glob to `zsh` (`no matches found`) or `bash` unless quoted.
## Cross-source queries
Both [Compose](#compose-source) and [Combine](#combine-combine) reduce per
source, then combine. Pick what the query needs.
| | Compose `source()` | Combine `iq combine` |
| --- | --- | --- |
| Shape | a single `jq` filter | a positional spec per source, then one `--with` program |
| Correlated reads(B keyed by A's rows) | ✓ nest `source()` | ✗ specs are independent |
| Quoting | sub-filter is a quoted string inside the filter | each spec is its own argument |
| Memory | each reduced result held until combine, plus a correlated `source()` re-runs per row | each reduced result held until combined |
Use `source()` when a read depends on another source's values, or to keep
everything in one composable filter.
Use `iq combine` for a straightforward join, union, or aggregate across a
few sources.
### Compose `source()`
`source("name"; "")` runs `` against source `name` (reduced, streamed
and pushed down like any query) and yields its results as a stream. A
one-argument `source("name")` yields the whole source.
Both arguments of `source()` are strings, so they must be quoted. It also
yields a stream. Collect it before indexing with `INDEX(source(…); .id)` or
`[source(…)]`, not `source(…) | INDEX(.id)`.
A filter that calls `source()` runs over a null input. Every read is an
explicit `source()` call, and there is no implicit primary source. As a
result, it needs no active source. Names resolve through the registry like `--src`, active-group
namespacing included.
```sh { title='Join users and orders in a single filter (no active source needed)' }
iq 'INDEX(source("users"; ".[]"); .id) as $u
| source("orders"; ".[] | select(.total > 99)")
| {name: $u[.userId].name, total}'
```
!!! tip "Correlated lookups re-run"
A `source()` opened inside a stream runs its sub-filter once per element.
The connection is reused, but the sub-filter re-executes.
Hoist a constant lookup into a binding `INDEX(source("users"; ".[]"); .id)
as $u | …` and index `$u` per element instead.
!!! tip "Optimize memory usage"
Binding `source()` to a jq variable materializes that call's whole result
set in memory (`jq` indexing needs a concrete array), even though the read
itself streams.
Push the reduction into the sub-filter, `source("orders"; ".[] |
select(.total > 99)")`, not `source("orders"; ".[]")` filtered outside, so
only the rows you need are held.
A bare `source("big")` over a large source buys no streaming benefit.
When each side is large and independent, prefer `iq combine`.
### Combine `combine`
`iq combine [=]... --with [flags]`
Query several sources and combine their results with one `jq` program.
Each positional is a source spec (`[=]`) reduced at the source
(bounded reads, streaming scans, and predicate pushdown all still apply). The
spec's results bind to a `jq` variable named after the source, with `/`, `.`
and `-` becoming `_` (so `prod/books=.[]` binds `$prod_books`). `--with` is
the final program, so it can join, union (`$a + $b`), aggregate, or fan across
any number of sources.
Each source reduces at the source, and a pushable filter pushes down. As a
result, this never copies whole datasets to join them. A spec with no filter
binds the whole keyspace, which is a whole-keyspace read. It needs
`--unbounded`, exactly as the same expression does on a plain query.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--no-compile` | ✗ | disable server-side predicate pushdown. Run each spec's filter client-side |
| | `--with ` | required | final jq over the bound source results (each spec's results bound to $name), run over a null input |
```sh { title='Join users with orders on a shared id, across two sources' }
iq combine 'users=.[] | {id, name}' \
'orders=.[] | select(.total > 99)' \
--with '($users | INDEX(.id)) as $u | $orders[] | . + {name: $u[.userId].name}'
```
!!! tip "Reduce, then combine"
Each spec is evaluated independently. Its (already reduced) result is held
in memory before `--with` runs. For this reason, keep a stage's output
small with `select`/projection/aggregation.
A spec that must materialize its whole source (`keys`, `.`, `map(...)`)
still needs `--unbounded`, exactly like a single-source query. A
`.[]`-rooted spec streams without it.
## Unbounded `--unbounded`
Permit a filter that loads the whole dataset into memory. It also materializes a
`.[]`-rooted filter instead of streaming it. As a result, a filter that
collapses the keyspace into one value (`keys`, `.`, `map(...)`) runs only with
it (see [Read strategies](how-it-works.md#read-strategies)).
`iq combine` and `iq data` carry their own copies of the flag where they apply.
## Client-side `--no-compile`
Disable pushdown.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--no-compile` | ✗ | disable predicate pushdown. Run the full `.[] | select()` filter client-side |
# Write data
docs/docs/write-data.md
# Write data
Data movement lives on the query command, like in `sq`. The `jq` filter is the
transform. `--insert` names a destination. Piped stdin is an implicit source.
There is no separate copy command. `iq exec` remains the untyped escape
hatch for anything the typed path does not cover.
## Insert `--insert`
With `--insert`/`--typed`, the filter transforms each item (its key is
preserved). Iteration over the source is implicit, so you do not write `.[]`.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--insert ` | ✗ | write each item into this destination source (copy/restore/import) instead of rendering |
| | `--typed` | ✗ | emit typed `{"key":…, "type":…, "value":…}` records, a re-importable dump (needed for Redis, a document store's plain output already restores) |
```sh { title='Source → Source, key/id preserving' }
iq --src books --insert books2
```
```sh { title='Cross-driver (Redis → Mongo), object values only' }
iq --src cache --insert docs
```
```sh { title='Back up Redis losslessly (typed dump)' }
iq --src cache --typed -o dump.jsonl
```
```sh { title='Back up Mongo with plain output (self-describing)' }
iq --src books --jsonl -o dump.jsonl
```
```sh { title='Add a dump file, then restore it into a live source' }
iq add file:///dump.jsonl -n snap
iq --src snap --insert cache
```
```sh { title='Move plan, no connection' }
iq --src books --insert books2 --explain
```
!!! warning "Implicit iteration"
This is the one place where `iq` reads a filter per item. Everywhere else
(a plain query and the `=` specs `iq combine`, `iq diff` and
`iq schema` take), the filter is rooted at the whole keyspace, and you
write `.[]` yourself.
The split is deliberate. A keyspace-rooted filter can aggregate across
items (`[.[] | .total] | add`) and can name a single key
(`.["orders:42"]`). A per-item filter can express neither. But when items
arrive from piped stdin, the write path has no keyspace at all.
Nothing can tell the two apart automatically (`.name` means the key named
"name" keyspace-rooted and the field `name` per item). For this reason,
they stay separate rather than guessing.
Existing keys are overwritten (upsert) unless `--no-overwrite` makes the
run insert-only. `--replace` empties the destination first (with
confirmation, or `--force`).
A document store (Mongo, CouchDB, Couchbase, Elasticsearch) stores each
value exactly as given and so requires it be a JSON object. A bare scalar
(for example a Redis string value) is rejected with a hint rather than
silently wrapped as `{"value": …}`. As a result, a successful copy
round-trips exactly. Shape it explicitly first, for example
`iq 'if type == "object" then . else {value: .} end' --insert `.
!!! note "Typed format"
`--typed` serializes the records in the chosen format: `--jsonl` (default),
`--json`, `--jsona`, or `--yaml`. All of these re-import through a
`file://` source or a piped `--insert`. `iq` auto-detects them from content
by their typed `{"key":…, "type":…, "value":…}` envelope.
A huge first record can defeat the content sniff. For this reason, a
`.yaml`/`.yml` name or an explicit `?format=` / `--from-format` remains
available as an override (`jsonl`, `yaml`, `mongoexport`, `bson`, `rdb`,
`dynamodb-json`, `cassandra-csv`, or `neo4j-json`). Aliases like `json` are
accepted.
The renderings that cannot carry a record back are rejected rather than
written. `--raw` (a scalar cannot hold the envelope), `--format parquet`
(columnar) and `--gron`/`--grona` (flattened assignments no source
decodes) drop `--typed` to grep or export the value stream instead.
## Key mapping `--key*`
On a plain copy, each item carries its source key, so no key flag is needed.
Foreign JSON (piped stdin, a reshaped stream) has no natural key. `--key-field`
takes it from an object field. `--key` computes it with a jq expression.
`--key-prefix` prepends a namespace either way.
`iq combine --insert` is the one write that always requires `--key` or
`--key-field`. A combine's results come out of one program over a null input,
so no value has a key to inherit. As a result, the run is refused up front
rather than failing partway through a copy. For the same reason, `combine` has
no `--typed`. A typed dump is a stream of `{key,type,value}` records and needs
the same key.
A write flag used without `--insert` is an error, never silently ignored.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--key ` | none | `jq` expression yielding each written item's key (--insert/--typed) |
| | `--key-field ` | none | object field to take each written item's key from (--insert) |
| | `--key-prefix ` | none | string prepended to every written key (--insert/--typed) |
```sh { title='Persist the joined rows into a third source' }
iq combine 'users=.[] | {id, name}' 'orders=.[] | select(.total > 99)' \
--with '($users | INDEX(.id)) as $u | $orders[] | . + {name: $u[.userId].name}' \
--insert joined --key '.userId | tostring' --key-prefix 'j:'
```
```sh { title='import foreign JSON from stdin, keyed by id' }
cat foreign.json | iq --insert books --key-field id
```
```sh { title='reshape + re-key while copying' }
iq '{t: .title}' --src books --insert kv --key '.t'
```
## Type mapping `--type`
A plain copy carries each item's native type along (a Redis hash lands as a
hash), so no type flag is needed. A filter that reshapes the value drops that
type. The output is plain JSON, and `--type` names the native type that the
destination stores it as.
Left unset, a typed destination infers it from the shape (Redis writes a scalar
as a `string` and an object or array as `json`). As a result, `--type` is only
required when you want something else:
- A `hash` built from an object
- A `list` or `set` from an array
- A `zset` from `[{member, score}]` pairs.
A document store ignores it. Every value is a document there. With `--typed`,
the same flag stamps the `type` field of each dump record instead.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--type ` | none | native type stamped on each written value, for example hash, list, json (--insert/--typed) |
```sh { title='Reshape Mongo documents into Redis hashes' }
iq '{title, year: (.year | tostring)}' --src books --insert cache --type hash
```
```sh { title='Project one field per item into a Redis list' }
iq '[.tags[]]' --src books --insert cache --type list --key-prefix 'tags:'
```
```sh { title='Same reshape, no --type: an object lands as RedisJSON' }
iq '{title, year}' --src books --insert cache
```
```sh { title='Dump reshaped items with an explicit type tag' }
iq '{title}' --src books --typed --type json -o titles.jsonl
```
## Replace `--replace`
By default, a write upserts. Existing keys are overwritten, and everything else
in the destination stays. `--replace` turns the copy into a restore. It empties
the destination first (the same operation as `iq data clear`, a Redis
`FLUSHDB`, a Mongo `deleteMany({})`) and then writes. As a result, the
destination ends up holding exactly the source.
Because it destroys data, it prompts (`clear before writing`).
`--force` answers yes. `--dry-run` reports the copy without clearing anything.
A destination that cannot be cleared (the read-only file dump) is refused up
front. `--no-overwrite` is the opposite choice, insert-only, so the two are
mutually exclusive. Neither applies to a `--typed` dump.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--replace` | ✗ | empty the destination before writing, with confirmation (--insert) |
| | `--force` | ✗ | skip the confirmation prompt for `--replace` |
| | `--no-overwrite` | ✗ | skip keys that already exist (--insert) |
```sh { title='Restore a dump so the destination matches it exactly' }
iq --src snap --insert cache --replace
```
```sh { title='Same, unattended (no prompt)' }
iq --src snap --insert cache --replace --force
```
```sh { title='Preview the restore: reports the copy, clears nothing' }
iq --src snap --insert cache --replace --dry-run
```
```sh { title='Fill gaps only, never touch an existing key' }
iq --src books --insert books2 --no-overwrite
```
## Lifecycle previews
Every `iq data` subcommand (`delete`, `clear`, `drop`) shares two previews.
`clear` and `drop` also prompt before destroying data. `--force` skips the
prompt.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--explain` | ✗ | print the access plan and exit without connecting or changing anything |
| | `--dry-run` | ✗ | connect and report the real effect without changing anything |
```sh { title='Plan only: which operation, and whether the driver supports it' }
iq data drop cache --explain
```
```sh { title='Connect and count what a clear would remove, then stop' }
iq data clear shop.orders --dry-run
```
```sh { title='Check which of the named keys exist before deleting' }
iq data delete cache book:1 book:2 --dry-run
```
## Delete `data delete`
`iq data delete ` removes a named set of keys, keeping the
container. It is the typed, capability-gated, explainable counterpart of a raw
per-key delete (HBase's `exec delete` verb, a Redis `DEL`).
Each key uses the same spelling as a *Get*: a bare string (`book:1`) or a JSON
array for a composite key (`["shop",42]`). A key already absent is not an error
(delete is idempotent). The report states it
(`deleted N key(s), M already absent`).
Unlike `data clear`/`data drop`, it does not prompt. The explicit key list you
typed is the confirmation (use `--dry-run` to preview). A backend with no
per-key identity (the read-only file dump) rejects it (like Redis rejects
`data drop`).
```sh { title='"deleted 2 key(s), 0 already absent"' }
iq data delete cache book:1 book:2
```
```sh { title='a composite-key row, by its JSON-array spelling' }
iq data delete shop.orders '["eu",42]'
```
## Clear `data clear`
Empties a container but keeps it (for example MongoDB `deleteMany({})` or Redis
`FLUSHDB`). Both this and `iq data drop` are distinct from `iq rm`, which only
unregisters a saved source. These destroy stored data, and prompt for
confirmation unless `--force` is used.
```sh { title='Empty a Redis source called "cache" (FLUSHDB)' }
iq data clear cache
```
```sh { title='Empty two MongoDB collections "shop.orders" and "shop.users"' }
iq data clear shop.orders shop.users
```
## Drop `data drop`
Removes the container entirely and prompts for confirmation unless `--force` is
used. Sources without a droppable container are rejected (for example Redis).
```sh { title='Reports drop as unsupported for Redis source called "cache"' }
iq data drop cache --explain
```
```sh { title='Drop one Mongo collection called "shop.orders"' }
iq data drop shop.orders
```
```sh { title='Drop containers "shop.orders" and "shop.users"' }
iq data drop shop.orders shop.users
```
## Dry run `--dry-run`
Reports the effect without writing. It does everything except the final
mutation. It connects, opens source and destination, and runs the real scan. It
applies the real transform and probes capabilities. Then it suppresses the
write.
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| | `--dry-run` | ✗ | report the effect of `--insert` without writing anything |
```sh { title='Dry run for a copy into "books2"' }
iq --src books --insert books2 --dry-run
```
```sh { title='Dry run for clearing source "books"' }
iq data clear books --dry-run
```
# Output
docs/docs/output.md
# Output
Results print as pretty JSON by default. A single format flag selects another rendering.
The flags are mutually exclusive. They apply to the jq read path and to `iq combine`, but not to
`exec` (which prints the backend's native reply).
## Format flags
Shorthand flags are available for most formats.
`-f`, `--format ` selects the same renderings by name (`json`, `jsonl`,
`jsona`, `yaml`, `values` with the alias `raw`, `gron`, `grona`, `parquet`).
It is mutually exclusive
with them, so `-f json --jsonl` is rejected.
`parquet` has no shorthand flag. It is a binary columnar format, selected by name only.
### JSON `--json`
Pretty JSON, one value per result (default). Shorthand `-j`.
```
iq '.[]' -j
```
```
{
"_id": "1",
"author": "Donovan and Kernighan",
"price": 39,
"tags": [
"go",
"programming"
],
"title": "The Go Programming Language",
"year": 2015
}
{
"_id": "2",
"author": "Martin Kleppmann",
"price": 45,
"tags": [
"data",
"architecture"
],
"title": "Designing Data-Intensive Applications",
"year": 2017
}
```
### JSON Lines `--jsonl`
Compact JSON, one value per line. Shorthand `-J`.
```
iq '.[]' -J
```
```
{"_id":"1","author":"Donovan and Kernighan","price":39,"tags":["go","programming"],"title":"The Go Programming Language","year":2015}
{"_id":"2","author":"Martin Kleppmann","price":45,"tags":["data","architecture"],"title":"Designing Data-Intensive Applications","year":2017}
```
### JSON Array `--jsona`
Every result wrapped in one array `[ ... ]`. Shorthand `-A`.
```
iq '.[]' -A
```
```
[
{
"_id": "1",
"author": "Donovan and Kernighan",
"price": 39,
"tags": [
"go",
"programming"
],
"title": "The Go Programming Language",
"year": 2015
},
{
"_id": "2",
"author": "Martin Kleppmann",
"price": 45,
"tags": [
"data",
"architecture"
],
"title": "Designing Data-Intensive Applications",
"year": 2017
}
]
```
!!! info "`iq --jsona` differs from `sq --jsona`"
`iq --jsona` wraps the whole result stream in one array (like `jq -s`). It
is the analogue of `sq --json`.
`sq --jsona` instead emits one JSON array *per row* with the keys dropped.
This is a columnar projection that a heterogeneous `jq` value stream has no
exact analogue for. As a result, `iq` keeps the `sq` flag name but its own
behaviour.
### Raw `--raw`
Unquoted scalars, one per line. Objects and arrays fall back to compact JSON.
Shorthand `-r`.
```
iq '.[].title' -r
```
```
The Go Programming Language
Designing Data-Intensive Applications
```
### YAML `--yaml`
YAML documents, separated by `---`. Shorthand `-y`.
```
iq '.[]' -y
```
```
_id: "1"
author: Donovan and Kernighan
price: 39
tags:
- go
- programming
title: The Go Programming Language
year: 2015
---
_id: "2"
author: Martin Kleppmann
price: 45
tags:
- data
- architecture
title: Designing Data-Intensive Applications
year: 2017
```
### gron `--gron`
Flattened `json.path = value;` assignment statements, one per line. Shorthand `-g`.
It is greppable and reversible with `gron --ungron`[^1]. Each result is rooted at a repeated `json`.
```
iq '.[]' -g
```
```
json = {};
json._id = "1";
json.author = "Donovan and Kernighan";
json.price = 39;
json.tags = [];
json.tags[0] = "go";
json.tags[1] = "programming";
json.title = "The Go Programming Language";
json.year = 2015;
json = {};
json._id = "2";
json.author = "Martin Kleppmann";
json.price = 45;
json.tags = [];
json.tags[0] = "data";
json.tags[1] = "architecture";
json.title = "Designing Data-Intensive Applications";
json.year = 2017;
```
!!! note "Paths"
`--gron` (and `--grona`) emit one `path = ;` statement per
line, object keys sorted.
A key that is an ASCII identifier(`^[A-Za-z_$][A-Za-z0-9_$]*$`) follows
a bare dot (`json.name`). Any other key is bracketed and JSON-quoted
(`json["odd key"]`). This is a deliberate ASCII subset of gron's rule,
because over-quoting stays ungron-safe.
`--gron` repeats the `json` root for every result, so ungron[^1] is
last-write-wins across results.
!!! warning "Typed dumps are not supported"
`--typed` rejects `--gron` and `--grona`. A flattened assignment stream is
a rendering to grep, not a dump. No source re-imports it. To gron the value
stream, drop `--typed`. To dump instead, use `--jsonl` (default), `--json`,
`--jsona`, or `--yaml`.
### gron Array `--grona`
Like `--gron` but result N roots at `json[N]`, so the whole stream ungrons[^1] back
to one JSON array (gron's `--stream` style). Shorthand `-G`.
```
iq '.[]' -G
```
```
json = [];
json[0] = {};
json[0]._id = "1";
json[0].author = "Donovan and Kernighan";
json[0].price = 39;
json[0].tags = [];
json[0].tags[0] = "go";
json[0].tags[1] = "programming";
json[0].title = "The Go Programming Language";
json[0].year = 2015;
json[1] = {};
json[1]._id = "2";
json[1].author = "Martin Kleppmann";
json[1].price = 45;
json[1].tags = [];
json[1].tags[0] = "data";
json[1].tags[1] = "architecture";
json[1].title = "Designing Data-Intensive Applications";
json[1].year = 2017;
```
!!! note "Paths"
`--grona` roots result **N** at `json[N]` under a leading `json = [];`, so
ungron[^1] rebuilds the full array (an empty stream ungrons[^1] to `[]`, like
`--jsona`).
### Parquet `--format parquet`
Streams the result values to an [Apache Parquet](https://parquet.apache.org/)
file ([Apache Arrow](https://arrow.apache.org/) columnar format). Parquet is the
bridge to [pandas](https://pandas.pydata.org/), [Polars](https://pola.rs/),
[DuckDB](https://duckdb.org/), and the wider data-science ecosystem.
Because it is binary, `iq` refuses to write it to a terminal. Redirect it or use `-o out.parquet`. A pipe or file is required.
```
iq '.[]' --format parquet -o out.parquet
```
```
iq '.[]' --format parquet > out.parquet
```
```bash
iq '.[]' --format parquet | python3 -c "
import sys, pyarrow.parquet as pq, io
table = pq.read_table(io.BytesIO(sys.stdin.buffer.read()))
print(table)
"
```
!!! note "Schema"
The schema is inferred from the first 1000 result values (schema-inference sample). It is
projected onto Arrow types:
- `integer→int64`
- `number→float64`
- `boolean→bool`
- `string→utf8`
- A `date-time` string→`timestamp[ns, UTC]`
- A `date` string→`date32`
- `object→struct`
- `array→list`
- An id-keyed map→`map`.
A column whose sampled shape is heterogeneous or
null-only falls back to the `arrow.json` canonical extension (utf8 storage holding byte-lossless
canonical JSON), marked in field metadata.
The Arrow schema is embedded in the file (`ARROW:schema`), so exact types survive a read-back.
A value that does not fit its inferred column type past the sample fails the export, naming
the column. For fully heterogeneous data, switch to `--format jsonl` rather than coercing.
!!! info "Presence caveat"
Arrow's validity bitmaps cannot distinguish a *missing* field from a field
present as `null`. Both collapse to a null in the column.
`iq` preserves the distinction inferred from the sample in field metadata
(`iq:presence` = `required` | `optional`), so it survives in the schema
even though the values collapse.
!!! warning "Typed dumps are not supported"
`--typed` dumps cannot use `parquet` (they carry a `{key,type,value}`
envelope). Run the query without `--typed` to export a columnar file.
## Compact `--compact`
Collapses the pretty renderings to single-line: `--json` becomes one compact
value per line (equivalent to `--jsonl`) and `--jsona` becomes a single-line
`[ ... ]`.
It is a no-op for `--jsonl`, `--raw`, `--yaml`, `--gron`, `--grona`, and
`--format parquet`, which are already condensed or binary (`--gron` and
`--grona` are inherently line-based).
## File `--output `
`--output` :material-earth:{ title="Global flag" } is global. Every command honours it (for example
`iq inspect -o report.json`). See [Global flags](global-flags.md#global-flags).
Writes results to `` instead of stdout, truncating an existing file, with the
shorthand `-o`. It is orthogonal to the format flags.
Color is off for a file unless you force it with `-C`. Progress and errors
still go to stderr.
## Numbers `--format.decimal`
`--format.decimal` :material-earth:{ title="Global flag" } is global.
`--format.decimal ` chooses how a **non-integer decimal**
from the backend is presented to the filter.
| value | behavior |
| --- | --- |
| `auto` (default) | each backend keeps its faithful form. MongoDB `Decimal128` is an exact string. A Redis fractional number is a `float64` |
| `number` | decimals become bare numbers (`float64`), convenient for arithmetic but lossy beyond `float64` |
| `string` | decimals become their exact literal as a string, precision-safe. Use `tonumber` to compute |
```bash { title='"19.99" — exact, precision-safe' }
./iq '.book.price' --format.decimal=string
```
```bash title="Compute on the exact decimal"
./iq '.book.price | tonumber * 1.2'
```
```bash title="Bare numbers, ready for jq arithmetic"
./iq '.[].price' --format.decimal=number
```
!!! warning
Because the `jq` filter runs client-side over the fetched value, this choice is
made at normalization time. It changes what the filter computes on, not only
how the result prints (unlike `sq`, where `jq` is not involved).
!!! note
Integers are always exact regardless of the mode. They arrive as an `int`, or
as a big integer when they exceed 64 bits. As a result, `.count + 1` stays
exact rather than rounding through `float64`.
A backend can round before `iq` sees the value. For example, RedisJSON stores
an integer larger than 64 bits as a double, so it arrives already in scientific
notation.
A big integer renders as a bare number in the JSON formats but as a quoted
string under `--yaml` (a `yaml.v3` limitation). Exactness is kept in
preference to YAML's numeric form.
## Color `--color`
`--color` :material-earth:{ title="Global flag" }, shorthand `-C`, is global.
Output is syntax-highlighted when `iq` writes to a terminal. It is left plain when
it is piped or redirected, so captured output stays free of color codes. TTY detection is
where capture safety comes from.
```bash title="Colored on a terminal, plain when piped"
./iq '.[]'
```
```bash title="Never colored"
./iq '.[]' -M
```
```bash title="Keep color through a pager"
./iq '.[]' -C | less -R
```
!!! note "Rendering"
Every rendering syntax-highlights on a terminal (`--json`, `--jsonl`,
`--jsona`, `--yaml`, `--raw`, `--gron`, `--grona`). Under `--raw`, strings
and nulls still print bare and uncolored, keeping shell substitution exact.
The human commands color their signal too:
- `ping` shows `ok`/`error` in green/red.
- `diff` shows additions green, removals red, and changes yellow.
- `ls`/`inspect` highlight the active source and section headers.
The raw reply bodies from `exec` and `inspect` are colored in their native
form. For example, MongoDB gets JSON syntax highlighting, and Redis gets
redis-cli-style value tokens.
## No color `--monochrome`
`--monochrome` :material-earth:{ title="Global flag" }, shorthand `-M`, is global.
Disable colored output. Color is on by default only when writing to a terminal.
!!! tip "NO_COLOR"
Colored output is also disabled if the `NO_COLOR` environment variable is
set. `-C` forces colored output and overrides `NO_COLOR`.
[^1]: `gron` flattens JSON into one `json.path = value;` assignment per line, so it can be grepped. `gron --ungron` reverses that and rebuilds the JSON from the assignments. This is what makes `--gron` output round-trippable. https://github.com/tomnomnom/gron#ungronning
# Global flags
docs/docs/global-flags.md
# Global flags
Flags every `iq` command accepts, a query, `iq inspect`, `iq diff`, `iq data drop`
alike.
Each one is documented on the page that owns its subject, and carries a
:material-earth:{ title="Global flag" } there to mark that it works everywhere.
| short :material-flag-outline: | long :material-flag-outline: | documented in |
| --- | --- | --- |
| `-s` | `--src` | [Sources](sources.md#showset-src) |
| | `--config` | [Configuration](configuration.md#configuration-config) |
| `-o` | `--output` | [Output](output.md#file-output-file) |
| `-C` | `--color` | [Output](output.md#color-color) |
| `-M` | `--monochrome` | [Output](output.md#no-color-monochrome) |
| | `--format.decimal` | [Output](output.md#numbers-formatdecimal) |
| | `--no-cache` | [Drivers](drivers.md#file-dumps) |
| | `--no-cache-index` | [Drivers](drivers.md#file-dumps) |
| | `--no-progress` | [Diagnostics & Logging](diagnostics-and-logging.md#flags) |
| | `--log*` | [Diagnostics & Logging](diagnostics-and-logging.md#flags) |
| | `--error*` | [Diagnostics & Logging](diagnostics-and-logging.md#flags) |
| | `--debug.pprof` | [Diagnostics & Logging](diagnostics-and-logging.md#flags) |
| `-v` | `--verbose` | [Diagnostics & Logging](diagnostics-and-logging.md#flags), plus [Query plan](query-plan.md#query-plan) for the formatted plan and [Sources](sources.md#list-ls) for the extra listing columns |
The three below have no topic page of their own.
## Timeout `--timeout`
Timeout for the whole operation, a query, a diff, a combine, or an `--insert`
copy (default is `5s`).
## Help `--help`
Display help on the command line for `iq`.
## Version `--version`
Prints the bare version and exits, with no name or prefix. As a result, a script
can use it directly (`v=$(iq --version)`). The version has one of these forms:
- `v1.2.3` for a release build
- The git-describe form (`v1.2.3-14-gabc1234`) for an untagged build
- `dev+` only for a plain `go build` with no version metadata at all.
It always writes to stdout, unaffected by `--output`. For the human form
(version, commit, build date, Go version), run `iq version`.
## Not global
The query command carries flags of its own, `--unbounded` and `--no-compile` on
[Query data](query-data.md#query-data).
`--explain` on [Query plan](query-plan.md#query-plan).
`--from-format` on [Write data](write-data.md#write-data) and the output-format
and write families on their own pages.
`iq combine` and `iq data` carry their own copies of `--unbounded`/`--explain`
where they apply.
# Query plan
docs/docs/query-plan.md
# Query Plan
`--explain` prints a formatted **query plan** and exits without connecting or
executing.
`-v`/`--verbose` prints the same plan to stderr, then runs, tracing each
backend command.
## Examples
```bash title="Plan only, no connection"
./iq --src orders '.[] | select(.total > 99) | {id, total}' --explain
```
```bash title="Annotated plan, a note per pipe stage"
./iq --src orders '.[] | select(.total > 99) | {id, total}' --explain -v
```
```bash title="Redis SCAN + typed reads"
./iq --src cache '.[] | select(.active)' --explain
```
```bash title="One pushed, one client-side"
./iq --src orders '.[] | select(.total > 99 and (.active | not))' --explain
```
```bash { title='Plan + live "mongo> find(...)" trace' }
./iq --src orders '.[] | select(.total > 99)' -v
```
```bash title="Trace on stderr, stdout stays pure data"
./iq '.[]' -v 2>/dev/null
```
## Breakdown
The plan shows four things, syntax-highlighted when the destination is
a terminal:
- **Filter**
- The `jq` filter pretty-printed with real line breaks. Nested
`source("name"; "")` sub-filters and every `iq combine` fragment
are formatted too.
- Under `-v`/`--verbose`, each top-level pipe stage also carries
a short right-aligned note describing it (`— keep inputs where …`). The stage
that reads from the store is marked with its route, colored by cost.
- A green `bounded read` (keyed lookup)
- A yellow `streaming scan` (batched over `.[]`)
- A red `materialized scan` (an aggregate, a non-`.[]` root, or any scan under
`--unbounded`)
- **Access Plan**
- The concrete backend calls each source will make, derived
from the filter's route (bounded keys, streaming scan, or materialize)
- **Pushdown**
- The breakdown: one line per top-level `select(...)` conjunct. Each line
says whether the backend evaluates it (`pushed`) or it re-runs
client-side (`client-side`). It also says why a client-side conjunct did
not push.
- A conjunct is `client-side` when the compiler cannot express it as
a provable superset (an inexact negation, a non-portable regex, an unsafe
field name, or any other unpushable construct). It is also `client-side`
when the backend's translator declines the compiled predicate (a range on
Elasticsearch, say), or when the source does no server-side filtering at
all (the read-only file driver).
- This is the observable split of what the backend evaluated versus what
ran client-side. It is absent under `--no-compile`.
- **Compiled Filter**
- As JSON, the merged fragment of the pushed conjuncts.
!!! danger "Redaction"
Credentials are never traced (for example Redis `AUTH` and the MongoDB auth
handshake are redacted or skipped).
!!! info "Explain is a dry run"
`--explain` never opens a connection, so it works offline against any saved
source.
!!! note "Verbosity"
Under `-v`, the live trace shows the actual commands (`redis> TYPE …`,
`mongo> find …`) as they run.
# Configuration
docs/docs/configuration.md
# Configuration `config`
The config file lives at `/iq/iq.toml`. `IQ_CONFIG` points
it elsewhere, and `--config` :material-earth:{ title="Global flag" } overrides both for one run (precedence:
`--config` > `IQ_CONFIG` > default). It holds the saved sources and the stored option
defaults below. `iq config location` prints the resolved path.
iq writes the config file with mode `0600`, so only you can read it. A source
saved with `--store inline` (the default) keeps its password in this file. If
the file holds an inline password and other users can access it, for example
after an edit or a copy, every command prints a warning to stderr with the fix
(`chmod 600 `) and then runs as usual. Windows has no such mode, so there
is no warning there. `iq config keyring migrate` moves inline passwords into the
OS keyring.
Inspect the config file and manage stored option defaults. Persist a flag's
value once so you need not retype it. Set an option globally, or scope it to
one source with `--src`.
At query time, the precedence is **explicit flag > per-source option > base
option > built-in default**. As a result, a saved default fills any flag you
leave unset. An explicit flag on the command line always takes precedence.
```sh { title='Every query defaults to YAML output' }
iq config set format yaml
```
```sh { title='30s timeout only when querying "prod"' }
iq config set --src prod timeout 30s
```
```sh { title='Effective value for "prod" (source > base > default)' }
iq config get --src prod format
```
```sh { title='Every persistable option: value, default, and help' }
iq config ls -v
```
```sh { title='Renders YAML (the stored default)' }
iq '.[]'
```
```sh { title='Explicit flag overrides the stored default' }
iq -f json '.[]'
```
!!! note "Persistable options"
Persistable options are the flags whose default it is reasonable to
persist:
- `--format`, `--format.decimal`, `--compact`
- `--timeout`
- `--monochrome`, `--color`, `--no-progress`
- `--no-cache`, `--no-cache-index`
- `--verbose`, `--log*`, `--error*`
Per-invocation flags are not storable:
- `--src`
- `--explain`
- `--unbounded`
- `--no-compile`
- `--reveal`, `--expand`
- `--debug.pprof`
An `iq combine` query has no single source, so it uses the base options
only, never a per-source override.
!!! warning "Logging options"
The `log*` options are the one place where a stored default and the
environment overlap. A stored `log*` value fills an unset flag. But an
`$IQ_LOG*` environment variable still wins over it (the `flag > env > default` chain
for logging applies before a stored default is treated as "set").
An explicit `--log*` flag beats both. No other option reads the
environment, so this interaction is unique to the logging family.
## Edit `edit`
Open the config file in `$IQ_EDITOR` (then `$VISUAL`, `$EDITOR`, else `vi`).
```sh
iq config edit
```
## Get `get`
Print an option's effective value at that scope.
```sh
iq config get [--src ]
```
## Keyring `keyring`
Manage the OS-keyring secrets that back `--store keyring` sources.
```sh
iq config keyring
```
### List `ls`
List keyring-backed sources, each marked `present` or `missing` (`-j`/`-y` for
machine-readable output).
```sh
iq config keyring ls
```
### Get `get`
Print a source's secret, redacted unless `--reveal` is used.
```sh
iq config keyring get
```
### Set `set`
Write or update a secret (reads stdin/prompt when the value is omitted). A
source with an inline password is left untouched. Use `migrate` for it.
```sh
iq config keyring set [value]
```
### Remove `rm`
Delete a source's secret and mark it inline again.
```sh
iq config keyring rm
```
### Migrate `migrate`
Move an inline password into the keyring, rewriting the stored URI to its
password-less form.
```sh
iq config keyring migrate [] [--all] [--dry-run]
```
### Prune `prune`
Delete stale entries left for non-keyring sources. The keyring cannot be
enumerated, so entries whose source was deleted are undetectable and are not
pruned.
```sh
iq config keyring prune [--dry-run]
```
## Location `location`
Print the resolved config file path.
```sh
iq config location
```
## List `ls`
List the options set at that scope. `-v` lists every persistable option with
its effective value, built-in default, and help.
```sh
iq config ls [--src ]
```
## Set `set`
Validate and store a value (base, or per source). The value is checked exactly
as the flag checks it, so an invalid value is refused.
```sh
iq config set [--src ]
```
With `-D` or `--delete` it removes a stored value instead.
```sh
iq config set -D/--delete [--src ]
```
## View `view`
Dump the whole config as TOML, source URIs redacted like `iq ls`.
```sh
iq config view [--reveal] [--expand]
```
# Loading exports
docs/docs/loading-exports.md
# Loading exports
`iq --jsonl` writes one JSON document per line (JSON Lines), the format every
dataframe tool reads directly.
Normalization is what makes that read predictable:
- One canonical rendering per value
- Exact integers
- Decimal strings
- [RFC3339Nano](https://pkg.go.dev/time#pkg-constants) UTC timestamps[^1]
- Base64 binary
- An explicit `null` for an absent field rather than a placeholder.
This section is the consumer's side: loading an export into a data-science
stack without losing that fidelity. The whys below were checked against
[pandas](https://pandas.pydata.org/) 3.0, [Polars](https://pola.rs/) 1.42, and
[DuckDB](https://duckdb.org/) 1.5.
### pandas
```python
import pandas as pd
df = pd.read_json("dump.jsonl", lines=True, dtype_backend="pyarrow")
```
Pass `dtype_backend="pyarrow"`, not the default. pandas' default NumPy dtypes
have no nullable integer. As a result, the first `null` in an integer column
silently widens the whole column to `float64`. Then `7` becomes `7.0`, and any
exact integer past `2^53` is corrupted before you look.
The Arrow-backed path keeps a typed, null-safe `int64[pyarrow]` (missing values
read as `pd.NA`, not `NaN`). Decimal strings and RFC3339Nano stay strings that
you can lift to exact types:
```python
import pyarrow as pa
df["amount"] = df["amount"].astype(pd.ArrowDtype(pa.decimal128(20, 2))) # exact decimal
df["ts"] = df["ts"].astype("timestamp[ns, tz=UTC][pyarrow]") # nanosecond UTC
```
pandas still labels `pd.NA` semantics experimental. Prefer the Arrow backend, but pin your
pandas version rather than depend on the exact behaviour.
### Polars
```python
import polars as pl
df = pl.read_ndjson("dump.jsonl")
df.null_count() # O(1) per column, tracked in the validity bitmap
```
Polars has one missing value, `null`, uniform across every type. `NaN` is a float *value*, not
missingness. iq never emits `NaN` for an absent field, so `mean`, `min`, and `null_count` stay
correct on iq output. A gap is a `null` that statistics skip, never a `NaN` that changes the result.
### DuckDB
```sql
SELECT * FROM read_json_auto('dump.jsonl');
```
`read_json_auto` (aka `read_json`) infers a type per field and shreds nested documents into
`STRUCT`/`LIST` you query with dot- and list-access. Two edges of iq's output to know:
- **Timestamps need an explicit cast.** DuckDB's type sniffer accepts fractional seconds only to
millisecond precision. As a result, iq's nanosecond RFC3339Nano strings (`2026-07-18T12:34:56.123456789Z`)
are inferred as `VARCHAR`, not `TIMESTAMP` (observed on DuckDB 1.5.4, verify in your version).
Cast to `TIMESTAMP_NS` to keep the nanoseconds. Plain `TIMESTAMP` truncates to microseconds:
```sql
SELECT CAST(ts AS TIMESTAMP_NS) AS ts FROM read_json_auto('dump.jsonl');
```
- **The UNNEST trap.** `UNNEST` of an empty *or* `null` list yields zero rows. As a result, a
naive `SELECT id, UNNEST(tags) …` silently drops every record whose list is empty or null. Keep them
with a lateral left join back:
```sql
SELECT j.id, u.tag
FROM read_json_auto('dump.jsonl') j
LEFT JOIN LATERAL UNNEST(j.tags) AS u(tag) ON true;
```
### Splink (entity resolution)
[Splink](https://moj-analytical-services.github.io/splink/) needs these inputs:
- A per-record `unique_id`
- Column names that conform across the sources you link
- Dates truncated to `yyyy-mm-dd`
- Critically, *true nulls*, never empty-string placeholders.
iq's export already fits. The key rides in each record (a ready `unique_id`), and iq emits an
explicit `null` for an absent field. Prepare a source for Splink with the jq filter, which reshapes,
re-keys, and truncates dates in one pass:
```bash
iq '.[] | {unique_id: .id, name, dob: (.created_at | .[0:10])}' --src people --jsonl
```
For an entity-resolution consumer, the choice between *omitting* a field and writing an explicit
`null` is significant. They are different inputs to the match. Export explicit nulls (a jq object
constructor like `{name}` already writes `null` for a missing field). See
[Null vs missing](how-it-works.md#null-vs-missing) for how iq draws that line at each layer.
[^1]: RFC 3339 is the date and time format for use in Internet protocols, a profile of ISO 8601. RFC3339Nano is Go's layout for it with nanosecond precision, so every timestamp renders the same way in every export. https://datatracker.ietf.org/doc/html/rfc3339
# Cookbook
docs/docs/cookbook.md
# Cookbook
`iq` renders JSON-family formats only, no CSV and no tables, by design. Its
output is meant to compose, so you can pipe it into other tools.
## Piped input
```sh { title='Implicit stdin source' }
cat dump.jsonl | iq '.[]'
```
Stdin becomes the source only when nothing else resolves. An active source wins
over the pipe.
`--from-format` forces the decode when the content cannot be sniffed (a gzipped
or oddly-shaped dump).
## sq
[`sq`](https://sq.io) is a command-line tool giving jq-style access to SQL
databases and files like CSV or Excel.
### SQL → NoSQL
```sh { title='Load a table from sq to iq' }
sq -J @pg.actor | iq --insert cache --key-field actor_id
```
`sq` emits one JSON object per row (`-J`). `iq` keys each by the named field.
Foreign JSON always needs `--key-field` or `--key` (see
[Key mapping](write-data.md#key-mapping-key)).
### NoSQL → SQL
```sh { title='Load documents from iq to sq' }
iq --src books '.[]' --jsonl | sq --insert @dw.books .data
```
Piped data is sq's `.data` table, and the destination table is created if
missing.
### CSV or Excel
```sh { title='Render data into CSV or Excel with sq' }
iq --src books '.[]' --jsonl | sq -C .data
iq --src books '.[]' --jsonl | sq -x .data -o books.xlsx
```
`iq` itself renders no CSV. sq is the tabular bridge.
## quicktype
[`quicktype`](https://github.com/glideapps/quicktype) generates strongly-typed
models and serializers from JSON, JSON Schema, TypeScript, and GraphQL queries.
```sh { title='Create typed models with quicktype from live documents' }
iq --src books '.[]' --jsona | quicktype -l typescript --top-level Book -o book.ts
```
The schema-fed variant (`iq schema … | quicktype -s schema`, see
[Schema](sources.md#schema-schema)) types the whole inferred shape. This one
types what a query returned. Pipe a bounded result, not a scan of a
huge keyspace.
## gron
[`gron`](https://github.com/tomnomnom/gron) transforms JSON into discrete
assignments to make it easier to grep for what you want and see the absolute
path to it.
```sh { title='Grep the flattened view then unflatten it' }
iq --src books '.[]' -G | grep -i title | gron --ungron
```
`-G` (grona) indexes results as `json[N]`, so
[gron](https://github.com/tomnomnom/gron)'s `--ungron` rebuilds a JSON array.
`-g` roots every result at `json` and is for grepping only.
## Miller
[`mlr`](https://github.com/johnkerl/miller) is like awk, sed, cut, join, and
sort for data formats such as CSV, TSV, JSON, JSON Lines, and
positionally-indexed.
### JSONL → CSV
```sh { title='Convert JSONL to CSV with Miller' }
iq --src books '.[]' --jsonl | mlr --ijsonl --ocsv cat
```
## Drift guard
```sh { title='Schema drift check' }
iq diff prod.orders staging.orders --schema || echo "schema drift"
```
`iq diff` follows diff(1): exit 0 when the sources match, 1 when they differ.
As a result, it slots into a cron job or a pre-deploy check as-is.
# AI agents
docs/docs/agents.md
# AI agents
An AI agent that has to read or move data across a polyglot estate is the
reader `iq` is already shaped for. `iq` offers one binary and one language over
ten backends and their dump files. It is JSON in and JSON out (`--jsonl`,
`--compact`, `-M`, `--error.format json`), so nothing has to be scraped out of a
native shell's formatting.
`--explain` is a dry run that never connects, so a plan can be inspected before
a single byte moves. `--dry-run` reports the effect of a write without doing
it.
The destructive commands are capability-gated. As a result, one against a
backend that does not implement the port fails with a clear message instead of
emulating it. Every error is redacted, so a password in a source URI never
reaches a transcript.
Queries are read-only, and a filter that must materialize a whole keyspace is
refused unless `--unbounded` is passed.
## Skill
`iq` ships an [Agent Skill](https://agentskills.io), a single Markdown file,
[`skills/iq/SKILL.md`](https://github.com/zsltg/iq/blob/main/skills/iq/SKILL.md),
that any agent reading the Agent Skills format can load.
It teaches the workflow rather than the flag list:
- Finding the source before guessing at one
- `--explain` before every scan
- `--dry-run` before every write
- The machine-readable output flags and the JSON error shape
- Which filters need `--unbounded` and why
- The rules around writes, so `--replace` and `iq data clear`, `iq data drop`
and `iq data delete` run only on an explicit instruction.
The per-backend detail stays in `iq --help`, `man iq` and this site, so the
skill stays small enough to load beside the task.
Install it with the cross-agent installer. The installer fetches once into
`~/.agents/skills` and links it into every agent's skills directory it detects:
```sh
npx skills add zsltg/iq
```
With the GitHub CLI (2.90 or newer):
```sh
gh skill install zsltg/iq
```
Without either installer, copy the raw file into wherever the agent in use
looks for skills:
```sh
mkdir -p ~/.claude/skills/iq
curl -fsSL https://raw.githubusercontent.com/zsltg/iq/main/skills/iq/SKILL.md \
-o ~/.claude/skills/iq/SKILL.md
```
## MCP server
`iq mcp` serves the same query core as a
[Model Context Protocol](https://modelcontextprotocol.io) server, speaking
JSON-RPC over stdin and stdout.
It is the CLI's operations as tools, over the same saved sources and the same
engine. As a result, an agent that cannot run shell commands still gets the
whole command set. It targets the 2026-07-28 specification revision and negotiates back
to 2025-11-25 for an older client.
### Client configuration
The server is the binary itself, so a client only needs the command. The
inherited `--timeout` defaults to 5 seconds, which is short for a scan, so pass
a longer one. Every example below registers the same server, `iq mcp --timeout
30s`, under the name `iq`.
[Claude Code](https://claude.com/claude-code):
```sh
claude mcp add iq -- iq mcp --timeout 30s
```
[Codex CLI](https://openai.com/codex/) (the `--` separates the server command
from Codex's own options. The same entry can be written by hand as
`[mcp_servers.iq]` in `~/.codex/config.toml`):
```sh
codex mcp add iq -- iq mcp --timeout 30s
```
[Gemini CLI](https://geminicli.com) (the `--` matters here too. A `--timeout`
before it is Gemini's own connection timeout in milliseconds, not iq's):
```sh
gemini mcp add iq iq mcp -- --timeout 30s
```
[Cursor](https://cursor.com) (`.cursor/mcp.json` in the project, or
`~/.cursor/mcp.json` for every project), [Cline](https://cline.bot)
(`~/.cline/mcp.json` for the CLI, the MCP Servers panel's Configure tab in the
IDE extensions), [Antigravity](https://antigravity.google)
(`~/.gemini/config/mcp_config.json`, or `.agents/mcp_config.json` in the
workspace) and the Gemini CLI settings file (`~/.gemini/settings.json`) all
take the same `mcpServers` block:
```json
{
"mcpServers": {
"iq": {
"command": "iq",
"args": ["mcp", "--timeout", "30s"]
}
}
}
```
[Copilot](https://github.com/features/copilot) in VS Code (`.vscode/mcp.json`
in the workspace, or the user profile file behind the
`MCP: Open User Configuration` command) names the map `servers` and wants the
transport spelled out:
```json
{
"servers": {
"iq": {
"type": "stdio",
"command": "iq",
"args": ["mcp", "--timeout", "30s"]
}
}
}
```
[OpenCode](https://opencode.ai) (`opencode.json`) names it `mcp`, calls a stdio
server `local` and takes the command as one array:
```json
{
"mcp": {
"iq": {
"type": "local",
"command": ["iq", "mcp", "--timeout", "30s"],
"enabled": true
}
}
}
```
[pi.dev](https://pi.dev) ships no MCP client by design. It expects a CLI plus
a skill, which is exactly what `iq` and the [Skill](#skill) above are. Install
the skill, and pi drives the binary directly.
Any other client that takes a stdio server block needs the same two facts: the
command `iq` and the arguments `mcp --timeout 30s`.
The server inherits the saved sources and the keyring of whoever starts it.
Thus, point an agent at a config with only the sources it is allowed to use,
not your own:
```sh
iq mcp --config ~/.config/iq/agent.toml --timeout 30s
```
Register that config's sources with the same `iq add --config
~/.config/iq/agent.toml ...` you use anywhere else.
### Safety model
- **Read-only by default.** The write, exec and lifecycle tools exist only
behind `--allow`. A tool that is not allowed is never registered. It is
absent from `tools/list` and unknown to the server, so a client cannot call it
by name. `--allow writes` adds `iq_insert`, and `--allow exec` adds `iq_exec`.
`--allow destructive` adds `iq_data_clear`, `iq_data_drop` and
`iq_data_delete` (and permits `iq_insert`'s `replace`). The flag is
repeatable.
- **`--allow exec` is broad.** `iq_exec` forwards native commands verbatim, with
no preview and no confirmation. With `--allow exec`, the agent can do anything
that the database account can do, also write and delete. Give the agent a
read-only database user, unless it must write.
- **Every result is bounded.** `--max-items` (200) and `--max-bytes` (256 KiB,
roughly 64k tokens) are hard caps. A per-call `max_items` or `max_bytes` can
only lower them, never raise them. A capped result comes back with
`truncated: true` rather than an error, so the agent knows there was more.
- **Every call is bounded.** The inherited `--timeout` bounds each call, and a
per-call `timeout` can only shorten it.
- **Confirmations.** A real call to `iq_data_clear`, `iq_data_drop`,
`iq_data_delete` or `iq_insert`'s `replace` without `confirm: true` does not
proceed. `iq_exec` asks for no confirmation. Where the client can ask its user, the server returns an
input-required result carrying the question. The client then retries the call
with the answer. Where it cannot, the call comes back refused, naming what to
pass. The CLI's `--force` has no counterpart here. A confirmed call is the
confirmation.
- **`iq_explain` first.** It never connects, and it names the route and the
pushed-down conjuncts, so a plan can be read before a scan runs.
- **Errors are redacted.** Every failure is a tool result carrying the CLI's
`{"error":{"message","causes"}}` document with every connection URI redacted.
As a result, no raw driver error and no stored password reaches a transcript.
### Tools
`readOnly` marks a tool that never modifies anything. `destructive` marks one
that can. Every tool declares `openWorldHint: false`. The sources are a closed,
configured set. The annotations are display hints, not the gate. `--allow` is
the gate.
| Tool | Allowed by | Annotations | What it does |
| --- | --- | --- | --- |
| `iq_sources` | always | readOnly, idempotent | The handles this server can access, with each URI's password redacted |
| `iq_ping` | always | readOnly, idempotent | Round-trip one cheap backend command and report the time |
| `iq_explain` | always | readOnly, idempotent | The access plan for a filter, without connecting |
| `iq_query` | always | readOnly, idempotent | Run a jq filter. Returns `{items, count, truncated}` |
| `iq_inspect` | always | readOnly, idempotent | A backend's native introspection, optionally narrowed by `only` |
| `iq_schema` | always | readOnly, idempotent | A draft 2020-12 JSON Schema inferred from a sample |
| `iq_diff` | always | readOnly, idempotent | Compare two sources by data, stats, or inferred schema |
| `iq_insert` | `--allow writes` | destructive, idempotent | Copy items into another source. `no_overwrite` defaults to true |
| `iq_exec` | `--allow exec` | destructive | Forward a command to the backend verbatim |
| `iq_data_clear` | `--allow destructive` | destructive, idempotent | Empty a container, keeping it |
| `iq_data_drop` | `--allow destructive` | destructive, idempotent | Remove a container entirely |
| `iq_data_delete` | `--allow destructive` | destructive, idempotent | Remove named keys, keeping the container |
Every tool returns `structuredContent` against a declared `outputSchema`, plus
the same JSON in a text block for a client that reads only unstructured content.
`tools/list` is sorted by name and cacheable for an hour with a private scope,
because only a restart can change it.
### Limits
- stdio only. There is no HTTP transport, so the server is a child process of
its client and reachable by nothing else.
- The tool set is fixed at process start. Changing `--allow` means restarting
the server.
- The server advertises the `tools` capability alone. Prompts, resources,
sampling, roots and protocol-level logging are not implemented. The last three
are deprecated as of the 2026-07-28 revision. Diagnostics go to stderr, or to
the `--log` file, never to stdout, which carries the protocol and nothing
else.
- A long dump is not a good tool result. Cap it, or run the CLI and read the
file.
The whole manual is also served as one file,
[llms-full.txt](https://zsltg.github.io/iq/llms-full.txt), so an agent can read
every page of this site in a single fetch.
# Diagnostics & Logging
docs/docs/diagnostics-and-logging.md
# Diagnostics & Logging
Global flags, adopted from `sq`, control verbose output, file logging, error
rendering, and profiling.
They are cross-cutting concerns handled at the CLI boundary. They never change
query results.
## Flags
| flag | default | effect |
| --- | --- | --- |
| `-v`, `--verbose` | off | print diagnostics (source resolved, store opened, query complete with scan count and elapsed) to stderr, plus the [query plan](query-plan.md) and a live backend command trace (disables the progress spinner) :material-earth:{ title="Global flag" } |
| `--log` | off | enable logging to a file (also via `IQ_LOG`) :material-earth:{ title="Global flag" } |
| `--log.file` | `/iq/iq.log` | log file path. An empty value disables logging :material-earth:{ title="Global flag" } |
| `--log.level` | `DEBUG` | `DEBUG`, `INFO`, `WARN`, or `ERROR` :material-earth:{ title="Global flag" } |
| `--log.format` | `text` | `text` or `json` :material-earth:{ title="Global flag" } |
| `--error.format` | `text` | error output format: `text` or `json` :material-earth:{ title="Global flag" } |
| `--error.stack` | off | print the wrapped error cause chain to stderr (can include backend internals, credentials stay redacted) :material-earth:{ title="Global flag" } |
| `--no-progress` | off | disable the scan progress spinner (`-v` disables it too). See [Global flags](global-flags.md#global-flags) :material-earth:{ title="Global flag" } |
| `--error.format.text.verbose` | on | for a jq syntax error in text format, draw a caret span under the offending token :material-earth:{ title="Global flag" } |
| `--debug.pprof` | off | write a runtime profile of the whole run: `cpu`, `mem`, `block`, `mutex`, `goroutine`, `thread`, or `trace` :material-earth:{ title="Global flag" } |
:material-earth:{ title="Global flag" } marks a flag every `iq` command accepts (see [Global flags](global-flags.md#global-flags)).
!!! note "Logging"
The `--log*` flags also read the environment when the flag is not set. The
precedence is **flag > env > default**. The environment variables are
`IQ_LOG`, `IQ_LOG_FILE`, `IQ_LOG_LEVEL`, `IQ_LOG_FORMAT`.
`--log` writes structured records to a file (down to the chosen level,
always plain, never tinted). A source location is always redacted before
it is logged, so a stored credential never reaches a log file.
!!! note "Verbose"
`-v` writes a terse human stream to stderr (INFO and above). The stream is
tinted when stderr is a terminal. It follows the same `-M`/`-C`/`NO_COLOR`
decision as [colored output](output.md#color-color).
## Examples
```bash title="Verbose diagnostics on stderr"
./iq -v '.[]'
```
```bash title="Structured logs to a file"
./iq --log --log.file=/tmp/iq.log --log.format=json '.[]'
```
```bash title="Enable logging via the environment"
IQ_LOG=true IQ_LOG_FILE=/tmp/iq.log ./iq '.[]'
```
```bash title="Machine-readable errors"
./iq --error.format=json '.bad |'
```
```bash title="Runtime profile"
./iq --debug.pprof=cpu '.[]' && go tool pprof cpu.pprof
```
# Drivers
docs/docs/drivers.md
# Drivers
`iq` picks the backend from a source's URI scheme. The query core is
driver-agnostic, so further backends slot in behind the same port.
Each driver below documents these topics:
- Keyspace mapping
- Value encoding
- Predicate pushdown
- Raw commands.
## Driver list `driver ls`
| short :material-flag-outline: | long :material-flag-outline: | default | description |
| --- | --- | --- | --- |
| `-j` | `--json` | ✗ | emit machine-readable JSON |
| `-y` | `--yaml` | ✗ | emit machine-readable YAML |
| `-v` | `--verbose` | ✗ | list the readable file dump formats as well |
```sh { title='List registered backends' }
iq driver ls
```
```sh { title='List registered backends with all the readable file dump formats' }
iq driver ls -v
```
!!! note "Supported versions"
The `VERSIONS` column lists the range of backend server versions the
bundled client library supports.
### Guarantees
The per-driver blocks below differ in encoding and pushdown detail, but every backend honors the
same contract:
- **One URI, native nouns.** The URI scheme picks the driver. The keyspace rides in the URI as the
backend's own noun (`?collection=`, `?table=`, `?database=`, `?label=`/`?rel=`, `?index=`). A
query overrides it per run with the dotted `handle.` suffix (see [Sources](sources.md)).
- **One jq interface.** A bounded filter fetches exactly the named keys. A missing key reads as
`null`, never an error. A `.[]`-rooted filter streams the keyspace in bounded pages. A filter
that collapses the keyspace into one value materializes only behind `--unbounded` (see
[Read strategies](how-it-works.md#read-strategies)).
- **Pushdown never changes results.** A pushed predicate is only ever a conservative pre-filter.
Where the backend can filter, the pre-filter is server-side. Where the backend cannot, the
pre-filter is a client-side raw-byte prefilter that drops a provable non-match before decode
(Redis, on RedisJSON values, Elasticsearch/OpenSearch and Couchbase, over the residual their
server-side query cannot narrow). The full jq always re-runs client-side. As a result, output is
identical with or without the pushed predicate, and [`--explain`](query-plan.md) shows exactly what was pushed.
- **Capabilities are explicit.** Filtered scans, count estimates, writes, clear, drop and per-key
delete are opt-in ports. A backend implements what its model supports. A command against a
missing capability fails with a clear message instead of emulating it. For example, Redis, whose
DB index cannot be removed, has no `drop`. The read-only file dump has no per-key `delete`.
- **Values round-trip.** Every value normalizes to JSON under a frozen per-backend encoding
contract. A `--typed` dump restores through `--insert` losslessly (see
[Write data](write-data.md)).
- **Bounded and redacted.** `--timeout` bounds every backend call. `iq` redacts a URI's password
from every listing, log line and error.
- **Native commands.** `iq exec` speaks the backend's own language, verbatim where one exists
(Redis commands, Mongo command documents, CQL, PartiQL, Cypher, Mango, the Elasticsearch DSL).
Where none exists, `iq exec` speaks a small fixed verb set (HBase). See each driver's Raw
commands section. Every `iq` flag must come before `exec`. `iq` forwards everything
after `exec` to the backend untouched.
### Capabilities
Which opt-in ports each driver implements. A `—` is not a gap in the docs. For that port, the
command fails with a clear message rather than emulating what the backend cannot do.
| Driver | `data clear` | `data drop` | `data delete` | count estimate |
| --- | :---: | :---: | :---: | :---: |
| Cassandra | ✓ | ✓ | ✓ | — |
| Couchbase | ✓ | ✓ | — | — |
| CouchDB | ✓ | ✓ | ✓ | ✓ |
| DynamoDB | ✓ | ✓ | ✓ | ✓ |
| Elasticsearch / OpenSearch | ✓ | ✓ | ✓ | ✓ |
| HBase | ✓ | ✓ | ✓ | — |
| MongoDB | ✓ | ✓ | ✓ | ✓ |
| Neo4j | ✓ | — | ✓ | ✓ |
| Redis | ✓ | — | ✓ | ✓ |
| File dumps | — | — | — | — |
`data clear` empties a keyspace and keeps it. `data drop` removes the keyspace itself.
`data delete` removes named keys (see [Write data](write-data.md#delete-data-delete)).
The count estimate is a cheap metadata total, never a second scan. As a result, an unfiltered
scan can show its progress against a rough total. The estimate is a hint that can drift as the
keyspace changes. `iq` reads it only for an unfiltered scan, never for a pushed-down filtered one.
## Cassandra
[Apache Cassandra](https://cassandra.apache.org) is a distributed wide-column
store built for high write throughput across many nodes.
After you register a `cassandra://` source, the same jq interface works against a table. **The table is the keyspace, a row's primary key is the key and the row is the value**.
The keyspace comes from the URI path. The table comes from the URI's `?table=` (overridable per
run with a dotted `handle.table`). Multiple contact points are comma-separated. `?consistency=`
sets the read/write consistency level (default `QUORUM`).
```sh { title='Register a Cassandra source' }
iq add -n books 'cassandra://localhost:9042/iq?table=books'
```
```sh { title='Fetch the row whose primary key is 2' }
iq --src books '.["2"]'
```
```sh { title='Stream the table, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every primary key' }
iq --src books --unbounded 'keys'
```
```sh { title='Register with auth and multiple contact points' }
iq add -n cl 'cassandra://user:pass@n1,n2:9042/app?table=orders'
```
Cassandra columns are natively typed, so `.year > 2015` needs no `tonumber`. The
`--unbounded` / streaming rules are identical to every backend. The driver reads the table's schema once
at connect time, so it knows the primary-key columns and their types.
### Key encoding
A row's key is its **full primary key**, the partition-key columns followed by the clustering
columns. A single-column primary key renders as its bare value (`42`, a uuid, a text value, like a
Mongo `_id`). A composite primary key renders as a compact JSON array in schema order:
```sh { title='Fetch by a single-column key, bare' }
iq --src sales '.["US"]'
```
```sh { title='Fetch by a composite key ((country), id), a JSON array' }
iq --src sales '.["[\"US\",1]"]'
```
The array elements are the columns' string forms. On lookup, the driver coerces them back through
the schema, so a `bigint`/`varint` key round-trips without precision loss.
### Value encoding
Each column value is normalized to JSON by CQL type:
| CQL type | JSON shape |
| --- | --- |
| `text`, `varchar`, `ascii` | the string verbatim |
| `int`, `bigint`, `smallint`, `tinyint`, `counter` | number |
| `varint` | number when it fits, else its exact decimal string |
| `float`, `double` | number |
| `decimal` | exact string (or a number under `--format.decimal number`) |
| `boolean` | `true` / `false` |
| `uuid`, `timeuuid` | canonical string |
| `timestamp` | RFC 3339 string |
| `blob` | base64 string |
| `inet` | address string |
| `list`, `set` | array (a set is returned sorted) |
| `map` | object (non-text keys stringified) |
| missing / null cell | omitted (reads as `null` in jq) |
### Pushdown
By default, the driver translates the **equality** clauses of a `.[] | select(...)` filter into a
CQL `WHERE`. As a result, the cluster filters before rows reach iq. Only equality and same-column membership are pushed:
| `select(...)` clause | Pushed | CQL translation | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `a = ?` | a single top-level column that exists in the table |
| `.a == 1 or .a == 2` | ✓ | `a IN (?, ?)` | an `or` of equalities on one column |
| `E1 and E2` | ✓ | `AND` of the pushable parts | drops any conjunct it cannot push (widening) |
| ranges, regex, `has`, `length`, negations, nested paths | — | — | run client-side. Ranges are skipped because jq treats a missing field as the lowest value, which CQL cannot reproduce |
A pushed `WHERE` that does not resolve to the full partition key runs with `ALLOW FILTERING`. As a
result, the coordinator does the scan, an opt-in cost (it is shown in `--explain`). Pushdown never
changes results, only speed. The full jq always re-runs client-side, so a pushed filter is a
conservative pre-filter. Pass `--no-compile` to stream the whole table and filter entirely client-side.
### Raw commands
`iq exec` runs a CQL statement verbatim and prints the rows as JSON. It is the raw path for
server-side queries, DDL and administration that the jq read path does not cover:
```sh { title='Read the cluster version' }
iq --src books exec 'SELECT release_version FROM system.local'
```
```sh { title='Run a filtered CQL query' }
iq --src books exec "SELECT title FROM books WHERE year > 2015 ALLOW FILTERING"
```
`iq inspect` reads the system schema through these subcommands: `local` (cluster/version),
`tables` (the keyspace's tables), and `columns` (a table's columns). `--only` narrows to those
subcommands.
## Couchbase
[Couchbase](https://www.couchbase.com) is a distributed document database
combining a key-value engine with SQL++ queries.
After you register a `couchbase://` source, the same jq interface works against a
collection. **The collection is the keyspace, a document's ID is the key and the JSON document is the value**.
A Couchbase cluster nests bucket → scope → collection. The host is the cluster.
The bucket rides in the URI's `?bucket=` (required for keyspace work). The
collection is `?collection=`, which accepts `orders` or `sales.orders` (scope
defaults to `_default`). You can override it per run with a dotted
`handle.[scope.]collection`.
Switching buckets is a different source. Use `couchbases://` for TLS.
**Credentials travel in the URI userinfo** (SDK `PasswordAuthenticator`), so
`--store keyring` moves the password to the OS keyring exactly as for the other
backends.
The driver disables the SDK's application telemetry explicitly, so the tool
reports nothing back to the cluster.
```sh { title='Register a Couchbase source' }
iq add -n books 'couchbase://Administrator:password@localhost/?bucket=iq'
```
```sh { title='Fetch the document whose ID is "2"' }
iq --src books '.["2"]'
```
```sh { title='Stream the collection, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every document ID' }
iq --src books --unbounded 'keys'
```
```sh { title='Query a different collection in the same bucket' }
iq --src books.archive '.[]'
```
Couchbase documents are JSON, so values need no type coercion. Integers keep
exact precision (large ones never collapse to a float).
Couchbase has no per-key `iq data delete` yet (`clear` and `drop` work). Per-key
delete is a v1 follow-up.
The document ID is KV metadata, not part of the value, so it is never injected
into the document.
A non-JSON (binary) document is returned as a string on a `.["k"]` lookup and
skipped by a scan (the query service returns only JSON).
A bounded `.["k"]` lookup is a KV get. A scan is a **SQL++ keyset walk ordered
by `META().id`** (never OFFSET/LIMIT paging), so it streams with bounded
memory.
Scans read at `RequestPlus` consistency, so the tool sees its own just-written
documents (read-your-writes).
### Pushdown
By default, the driver translates the **equality**, **range** and **existence** clauses of a
`.[] | select(...)` filter into a SQL++ `WHERE` (with named parameters). As a result, the query
service filters before documents reach iq:
| `select(...)` clause | Pushed | SQL++ predicate | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `` `a` = $p `` | equality. A `null` literal widens to `` (`a` IS NULL OR `a` IS MISSING) `` |
| `.a > x` / `.a <= x` | ✓ | `` (`a` > $p OR ISSTRING(`a`) OR …) `` | range. `ISTYPE()` clauses widen it so that jq's cross-type ordering (null < bool < number < string < array < object) is reproduced. SQL++ comparison operators are type-restricted, so higher/lower-ranked types are re-included explicitly |
| `.a \| has` / `has("a")` | ✓ | `` `a` IS NOT MISSING `` | key presence, exact |
| `has("a") \| not` | ✓ | `` `a` IS MISSING `` | key absence, exact |
| `E1 and E2` | ✓ | `(… AND …)` | drops any conjunct it cannot push (widening) |
| `E1 or E2` | ✓ | `(… OR …)` | pushed only when **every** branch is pushable. An all-equality OR over one field collapses to `` `a` IN $p `` |
| `!=`, regex, `length`, `any`, nested-array tests | — | — | run client-side. The driver uses a plain keyset scan, because SQL++ semantics for these can wrongly exclude a document that jq keeps |
Every value rides as a named parameter, never concatenated. The driver validates
and backtick-quotes keyspace and field identifiers. As a result, nothing
user-supplied is ever interpolated raw.
Pushdown never changes results, only speed. The full jq always re-runs
client-side, so a pushed filter is a conservative pre-filter. `--explain` shows
the `WHERE`. `--no-compile` streams the whole collection and filters entirely
client-side.
Whatever the `WHERE` leaves behind, a **client-side raw-byte prefilter** runs
the full predicate over each row's raw value before it is decoded. The
prefilter drops any row that it can prove the predicate rejects.
So a fallback scan (a `!=`, a regex) or a partially-pushed scan (a dropped
conjunct) skips the dominant `UseNumber` decode of the documents that the query
service cannot exclude. The prefilter works on the bytes that the keyset scan
already returned. This is the same trick as the Redis and Elasticsearch
prefilters.
The prefilter is byte-level and never changes results (the full jq still
re-runs client-side). As a result, it is bypassed in the one case where it
gives no benefit. That case is when the `WHERE` already captured the predicate
exactly (the query service returned only matches).
A Couchbase document's ID is KV metadata, never injected into the value, so,
unlike the Elasticsearch prefilter, there is no injected-field case to disable
it.
**Index requirement.** A SQL++ scan needs an index on the collection.
On Server 7.6+, a sequential scan answers index-free queries automatically. On
7.0–7.5, or for large collections, create one:
`CREATE PRIMARY INDEX ON \`bucket\`.\`scope\`.\`collection\``. `iq` reports a "no index
available" error (code 4000) with exactly that hint.
### Raw commands
`iq exec` runs a raw [SQL++](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/index.html)
statement. The first argument is the statement. An optional second argument is a JSON object of
named parameters (bound end-to-end, never string-built):
```sh { title='Run a parameterized SQL++ query' }
iq --src books exec 'SELECT META(t).id, t.* FROM `iq` t WHERE t.year > $min' '{"min": 2015}'
```
```sh { title='Count the documents' }
iq --src books exec 'SELECT COUNT(*) AS n FROM `iq`'
```
`iq inspect` reads cluster and bucket metadata through these subcommands:
- `cluster` (nodes and services)
- `buckets` (the cluster's buckets)
- `collections` (the selected bucket's scopes and collections)
- `indexes` (the query indexes).
`--only` narrows to those subcommands.
## CouchDB
[Apache CouchDB](https://couchdb.apache.org) is a document database that
speaks HTTP and JSON, built around multi-master replication.
After you register a `couchdb://` source, the same jq interface works against a
database. **The database is the keyspace, a document's `_id` is the key and the document is the value**.
The host is the CouchDB server. The database rides in the URI's `?database=`
(overridable per run with a dotted `handle.database`, because one server hosts
many databases).
Use `couchdbs://` for TLS. **Credentials travel in the URI userinfo** (HTTP
basic auth), so `--store keyring` moves the password to the OS keyring exactly
as for the other backends.
```sh { title='Register a CouchDB source' }
iq add -n books 'couchdb://admin:password@localhost:5984/?database=iq'
```
```sh { title='Fetch the document whose _id is "2"' }
iq --src books '.["2"]'
```
```sh { title='Stream the database, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every _id' }
iq --src books --unbounded 'keys'
```
```sh { title='Query a different database on the same server' }
iq --src books.other '.[]'
```
CouchDB documents are JSON, so values need no type coercion. Integers keep
exact precision (large ones never collapse to a float). `_id` and `_rev` are
kept in the document. Design documents (`_design/…`) are database metadata, and
scans skip them. The `--unbounded` / streaming rules are identical to
every backend.
### Pushdown
By default, the driver translates the **equality**, **range**, **existence**, byte-safe
**regex** and **length** clauses of a `.[] | select(...)` filter into a Mango `_find` selector.
As a result, the server filters before documents reach iq:
| `select(...)` clause | Pushed | Mango selector | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `{"a": x}` | equality. A `null` literal also matches an absent field |
| `.a > x` / `.a <= x` | ✓ | `{"$or": [{"a": {"$gt": x}}, …]}` | range. `$type` clauses widen it so that jq's cross-type ordering (null < bool < number < string < array < object, matching CouchDB collation) is reproduced |
| `.a \| test("re")` | ✓ | `{"a": {"$regex": "re"}}` | byte-safe ASCII patterns only (below). A case-insensitive or non-ASCII-safe pattern runs client-side |
| `.a \| has` / `has("a")` | ✓ | `{"a": {"$exists": true}}` | key presence, exact |
| `has("a") \| not` | ✓ | `{"a": {"$exists": false}}` | key absence, exact |
| `.a \| length == n` | ✓ | `{"$or": [{"a": {"$size": n}}, {"a": {"$type": …}}, …]}` | jq `length` is polymorphic (array/string/object/number). As a result, per-type `$type` clauses widen the array `$size` to a superset. `n == 0` also matches null and a missing field |
| `E1 and E2` | ✓ | `{"$and": […]}` | drops any conjunct it cannot push (widening) |
| `E1 or E2` | ✓ | `{"$or": […]}` | pushed only when **every** branch is pushable |
| `!=`, `any`, nested-array tests, a case-insensitive or non-byte-safe regex | — | — | run client-side. The driver uses a plain `_all_docs` scan, because Mango's semantics for these can wrongly exclude a document that jq keeps |
**Byte-safe regex.** CouchDB's Mango `$regex` runs its Erlang engine over the document's raw UTF-8
bytes with no unicode option. It skips a non-string field (an `is_binary` guard, exactly as jq's
`test` over a non-string is false). A pattern is pushed only when it means the same byte-for-byte as
gojq's RE2. The pattern can use only these constructs:
- Pure-ASCII literals
- Anchors
- Quantifiers
- Groups
- Positive classes
- The `\d \w \s` shorthands.
An unescaped `.`, a negated class (`[^…]`, `\D`, `\W`, `\S`), any non-ASCII byte, or the
`i` flag is declined and runs client-side. The reason is that over multi-byte text, a byte engine
and a rune engine diverge. The subject string can be any Unicode. Only the pattern is constrained.
Pushdown never changes results, only speed. The full jq always re-runs client-side, so a pushed
filter is a conservative pre-filter. `--explain` shows the selector. `--no-compile` streams the
whole database and filters entirely client-side.
A pushed `_find` uses whatever Mango index fits (create one in CouchDB for large databases).
Without one, CouchDB warns and falls back to its built-in index.
### Raw commands
`iq exec` runs a raw [Mango `_find`](https://docs.couchdb.org/en/stable/api/database/find.html). The
argument is a JSON `_find` request (`{"selector":{…},"limit":…}`) or a bare selector (wrapped as
`{"selector":…}`). It prints the matching documents with the paging bookmark:
```sh { title='Run a Mango _find request' }
iq --src books exec '{"selector": {"year": {"$gt": 2015}}, "limit": 10}'
```
```sh { title='Run a bare selector, wrapped automatically' }
iq --src books exec '{"author": "Martin Kleppmann"}'
```
`iq inspect` reads server and database metadata through these subcommands:
- `server` (version and vendor)
- `databases` (the server's databases)
- `dbinfo` (the selected database's document count, sizes and update sequence)
- `indexes` (its Mango indexes).
`--only` narrows to those subcommands.
## DynamoDB
[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) is AWS's managed
serverless key-value and document database.
After you register a `dynamodb://` source, the same jq interface works against a table. **The table is the keyspace, an item's primary key is the key and the item is the value**.
The region is the URI host. The table rides in the URI's `?table=` (overridable per run with a
dotted `handle.table`). An optional `?endpoint=` points at DynamoDB Local.
**Credentials never travel in the URI.** The AWS default credential chain (environment, `~/.aws`,
IAM role) resolves them, so no secret touches the config or keyring.
```sh { title='Register a DynamoDB source, credentials from the AWS chain' }
iq add -n books 'dynamodb://us-east-1/?table=books'
```
```sh { title='Fetch the item whose partition key is 2' }
iq --src books '.["2"]'
```
```sh { title='Stream the table, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every primary key' }
iq --src books --unbounded 'keys'
```
```sh { title='Register DynamoDB Local via ?endpoint=, the driver supplies dummy credentials' }
iq add -n local 'dynamodb://us-east-1/?table=books&endpoint=http://localhost:8000'
```
DynamoDB attributes are natively typed, so `.year > 2015` needs no `tonumber`. The `--unbounded` /
streaming rules are identical to every backend. The driver reads the table's key schema once at connect
time, so it knows the partition and (optional) sort key and their types.
### Key encoding
An item's key is its **full primary key**, the partition key, then the sort key when the table has
one. A partition-key-only table renders the key as its bare value (`42`, a string, like a Mongo
`_id`). A table with a sort key renders a compact JSON array in schema order:
```sh { title='Fetch by a partition-key-only key, bare' }
iq --src sales '.["US"]'
```
```sh { title='Fetch by a composite key (partition "US", sort 1), a JSON array' }
iq --src sales '.["[\"US\",1]"]'
```
The array elements are the key attributes' string forms. On lookup, the driver coerces them back
through the key schema, so a numeric (`N`) or binary (`B`) key round-trips faithfully.
### Value encoding
Each attribute value is normalized to JSON by DynamoDB type:
| DynamoDB type | JSON shape |
| --- | --- |
| `S` (string) | the string verbatim |
| `N` (number) | integer as a number (exact string when it overflows int64), decimal as an exact string (or a number under `--format.decimal number`) |
| `BOOL` | `true` / `false` |
| `B` (binary) | base64 string |
| `NULL` | `null` |
| `M` (map) | object |
| `L` (list) | array |
| `SS`, `NS`, `BS` (sets) | array (of strings / numbers / base64 strings) |
| missing attribute | omitted (reads as `null` in jq) |
Note that the set types (`SS`/`NS`/`BS`) normalize to a plain array. As a result, a copy **back**
into DynamoDB writes them as a list (`L`), not a set. A number presented as a string (auto/string
decimal mode) writes back as a string (`S`). For a numeric round-trip, use
`--format.decimal number`.
### Pushdown
By default, the driver translates the **equality** and **existence** clauses of a
`.[] | select(...)` filter into a DynamoDB `Scan` `FilterExpression`. As a result, the service
filters before items reach iq. The driver references every attribute through a `#name`
placeholder, so a reserved word (`name`, `status`, `size`, `year`, …) is always safe:
| `select(...)` clause | Pushed | FilterExpression | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `#a = :v` | a single top-level attribute, string, number, or boolean literal |
| `.a \| has` / `has("a")` | ✓ | `attribute_exists(#a)` | key presence, exact |
| `has("a") \| not` | ✓ | `attribute_not_exists(#a)` | key absence, exact |
| `E1 and E2` | ✓ | `AND` of the pushable parts | drops any conjunct it cannot push (widening) |
| `E1 or E2` | ✓ | `OR` of the parts | pushed only when **every** branch is pushable |
| ranges, regex, `length`, `!=`, nested paths | — | — | run client-side. Ranges are skipped because jq orders a string above every number, which a typed DynamoDB comparison cannot reproduce |
A `Scan` reads the whole table (there is no `WHERE` on a primary-key membership like a relational
store). The `FilterExpression` only avoids shipping non-matching items over the wire. `--explain`
shows the cost. Pushdown never changes results, only speed. The full jq always re-runs
client-side, so a pushed filter is a conservative pre-filter. Pass `--no-compile` to stream the whole
table and filter entirely client-side.
### Raw commands
`iq exec` runs a [PartiQL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html)
statement verbatim and prints the items as JSON. It is the raw path for server-side queries and
writes that the jq read path does not cover:
```sh { title='Fetch one item by key with PartiQL' }
iq --src books exec 'SELECT * FROM "books" WHERE id = 2'
```
```sh { title='Run a filtered PartiQL query' }
iq --src books exec 'SELECT title FROM "books" WHERE "year" > 2015'
```
`iq inspect` reads table metadata through these subcommands: `tables` (the region's tables) and
`table` (the selected table's key schema, item count, size, billing mode and index names). `--only`
narrows to those subcommands.
## Elasticsearch & OpenSearch
[Elasticsearch](https://www.elastic.co/elasticsearch) and its fork
[OpenSearch](https://opensearch.org) are document search and analytics
engines built on Lucene, queried over HTTP with JSON.
After you register an `elasticsearch://` (or `opensearch://`) source, the same jq interface works against an
index. **The index is the keyspace, a document's `_id` is the key and its `_source` is the value**.
The host is the server. The index rides in the URI's `?index=` (overridable per run with a dotted
`handle.index`, because one server hosts many indices). Use `elasticsearch+s://` / `opensearch+s://` for
TLS.
**Credentials,
when the cluster needs them, travel in the URI userinfo** (HTTP basic auth). As a result,
`--store keyring` moves the password to the OS keyring exactly as for the other backends.
**OpenSearch is the same
driver** behind the scheme (two `iq driver ls` entries, with their own supported version ranges,
sharing one implementation). Everything below applies to both. The only differences are internal:
- The [opensearch-go](https://github.com/opensearch-project/opensearch-go) client, because
Elasticsearch's own client refuses non-Elasticsearch servers
- OpenSearch's point-in-time endpoint
- An `_id` keyset sort for scans, because OpenSearch predates Elasticsearch's `_shard_doc`.
```sh { title='Register an Elasticsearch source' }
iq add -n books 'elasticsearch://localhost:9200/?index=books'
```
```sh { title='Fetch the document whose _id is "2"' }
iq --src books '.["2"]'
```
```sh { title='Stream the index, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every _id' }
iq --src books --unbounded 'keys'
```
```sh { title='Query a different index on the same server' }
iq --src books.authors '.[]'
```
```sh { title='Register an OpenSearch source, identical surface' }
iq add -n logs 'opensearch://localhost:9201/?index=books'
```
Elasticsearch documents are JSON, so values need no type coercion. Integers keep exact precision
(large ones never collapse to a float). The driver injects each document's `_id` (Elasticsearch
metadata, stored outside `_source`) into the value as `_id`. As a result, a plain `.[]` stream is
self-describing and restorable, like a Mongo document. `--typed` is not needed for a lossless
backup.
The
`--unbounded` / streaming rules are identical to every backend. A scan pages the index with a
point-in-time and `search_after` (keyset pagination, sorted by `_shard_doc`), so it never re-reads
from an offset.
### Pushdown
By default, the driver translates the **equality** and **existence** clauses of a
`.[] | select(...)` filter into an Elasticsearch `bool` query. As a result, the cluster filters
before documents reach iq. The driver reads the index mapping once at connect, so an equality is
pushed only onto a field whose type matches it exactly. It is never pushed onto analyzed `text`,
where a term can wrongly exclude a match:
| `select(...)` clause | Pushed | Elasticsearch query | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `{"term": {"a": x}}` | a single top-level field mapped `keyword`/numeric/`boolean`/`ip` (or a `text` field's `.keyword` sub-field). The literal's type must match the field |
| `.a \| has` / `has("a")` | ✓ | `{"exists": {"field": "a"}}` | key presence, exact |
| `has("a") \| not` | ✓ | `{"bool": {"must_not": {"exists": …}}}` | key absence, exact |
| `E1 and E2` | ✓ | `{"bool": {"must": […]}}` | drops any conjunct it cannot push (widening) |
| `E1 or E2` | ✓ | `{"bool": {"should": […], "minimum_should_match": 1}}` | pushed only when **every** branch is pushable |
| `.a == null`, ranges, `!=`, `length`, regex, `any`, nested paths | — | — | run client-side. A range excludes a missing field and orders types unlike jq's cross-type ordering. `== null` matches absent-or-null (no single term does). An analyzed-text or unmapped field has no exact term |
Pushdown never changes results, only speed. The full jq always re-runs client-side, so a pushed
filter is a conservative pre-filter. `--explain` shows the query. `--no-compile` streams the
whole index and filters entirely client-side.
Whatever the `bool` query leaves behind, a **client-side raw-byte prefilter** runs the full predicate
over each hit's raw `_source` before it is decoded. The prefilter drops any hit that it can prove
the predicate rejects. So a fallback scan (a range, an equality on an unmapped or analyzed field) or
a partially-pushed scan skips the dominant `UseNumber` decode of the documents that the cluster
cannot exclude. The prefilter works on the bytes that `_search` already returned. This is the same
trick as the Redis prefilter.
The prefilter is byte-level and never changes results (the full jq still re-runs client-side). As a
result, it is bypassed in two cases where it gives no benefit or is wrong:
- The `term` query already captured the predicate exactly (the cluster returned only matches).
- The predicate references the injected `_id` field, which the raw `_source` does not carry.
### Raw commands
`iq exec` runs a raw [`_search`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html):
the argument is a JSON search body (`{"query":{…},"size":…,"aggs":…}`) or a bare query object
(`{"match":{"title":"dune"}}`, wrapped as `{"query":…}`). It prints the whole reply, hits,
aggregations and all, as JSON:
```sh { title='Run a raw range query' }
iq --src books exec '{"query": {"range": {"year": {"gt": 2015}}}}'
```
```sh { title='Run a bare match query, wrapped automatically' }
iq --src books exec '{"match": {"author": "Kleppmann"}}'
```
`iq inspect` reads server and index metadata through these subcommands:
- `server` (node, cluster and version)
- `indices` (the server's indices)
- `mapping` (the selected index's field mapping, which shows what a term pushdown can use)
- `aliases` (the server's aliases).
`--only` narrows to those subcommands.
## HBase
[Apache HBase](https://hbase.apache.org) is a distributed wide-column store
on Hadoop, modeled on Google Bigtable.
After you register an `hbase://` source, the same jq interface works against a table. **The table is the keyspace, a row key is the key and the row is the value**.
The URI host is the ZooKeeper quorum (comma-separated hosts, default port `2181`). The table rides
in the URI's `?table=` (a `namespace:table`, overridable per run with a dotted `handle.table`). The ZooKeeper znode parent
defaults to `/hbase`, overridable with `?znode=`.
```sh { title='Register an HBase source' }
iq add -n books 'hbase://localhost:2181/?table=iq_books'
```
```sh { title='Fetch the row whose key is "1"' }
iq --src books '.["1"]'
```
```sh { title='Stream the table, filtered on a nested family.qualifier' }
iq --src books '.[] | select(.cf.author == "Herbert")'
```
```sh { title='Materialize every row key' }
iq --src books --unbounded 'keys'
```
```sh { title='Register with a multi-host quorum, a namespaced table, and a znode parent' }
iq add -n zk 'hbase://z1,z2,z3:2181/?table=ns:events&znode=/hbase-unsecure'
```
A row is a **nested object**: `{family: {qualifier: value}}`, so a cell is addressed as
`.cf.title` in jq. The row key is the map key, not a field of the row.
### Value encoding
HBase stores **no types**. Every cell is raw bytes. As a result, the driver presents a value
**without a guess** by default and **exactly** when you declare its encoding:
| Column | Read as | Written from |
| --- | --- | --- |
| undeclared (default) | valid UTF-8 → the string verbatim, otherwise a base64 string | a string → its UTF-8 bytes |
| `?types=cf:q=text` | the string verbatim | a string → its UTF-8 bytes |
| `?types=cf:q=bytes` | a base64 string | a base64 string → raw bytes (lossless for binary) |
| `?types=cf:q=int` / `long` | a number (4- / 8-byte big-endian, the HBase `Bytes` layout) | a whole number → those bytes |
| `?types=cf:q=double` | a number (8-byte IEEE-754) | a number → those bytes |
| `?types=cf:q=bool` | `true` / `false` (1 byte) | a bool → one byte |
The driver **never guesses** a numeric type from bytes (an 8-byte string is indistinguishable from a
`long`). It either *knows* (you declared it) or *does not guess* (text, else base64). Declared columns
round-trip losslessly in both directions. An undeclared column read back as base64 (non-UTF-8 bytes)
does **not** round-trip through a write. For that round-trip, declare it `bytes`.
```sh { title='Declare the numeric columns so they read as numbers' }
iq add -n books 'hbase://localhost:2181/?table=iq_books&types=cf:year=long,cf:price=double'
```
```sh { title='Filter on a declared column, no tonumber needed' }
iq --src books '.[] | select(.cf.year > 2015) | .cf.title'
```
### Key encoding
The row key follows the same contract as a cell: text when it is valid UTF-8,
base64 otherwise. If `?keytype=` declares its encoding, the declared encoding
applies (`&keytype=long` reads and writes the 8-byte `Bytes` layout). As a
result, a declared key round-trips losslessly.
### Pushdown
By default, the driver translates the **column-equality** clauses of a `.[] | select(...)` filter
into an HBase server-side filter (`SingleColumnValueFilter`, combined with `MustPassAll` for an
`and`). As a result, the region servers filter before rows reach iq. The driver encodes the
equality literal through the column's declared type, so the comparison matches the stored bytes:
| `select(...)` clause | Pushed | HBase filter | Notes |
| --- | :---: | --- | --- |
| `.cf.q == x` | ✓ | `SingleColumnValueFilter(cf, q, =, x)` | a two-segment `family.qualifier` path. The literal must encode to the column's declared type (undeclared → string) |
| `E1 and E2` | ✓ | `FilterList(MustPassAll, …)` | drops any conjunct it cannot push (widening) |
| `.cf.q \| has`, ranges, regex, `length`, `!=`, `or` | — | — | run client-side. Existence and `or` are not pushed because HBase has no clean superset-safe filter for them. Ranges cannot reproduce jq's cross-type ordering |
A row-key point read (`.["1"]`) is a direct `Get`, not a scan. Pushdown never changes results, only
speed. The full jq always re-runs client-side, so a pushed filter is a conservative pre-filter. Pass
`--no-compile` to stream the whole table and filter entirely client-side. `--explain` shows the
cost.
### Raw commands
HBase has **no query language**. As a result, `iq exec` is a small, safe verb set mapped straight
onto RPC, never a built query string. This makes it injection-safe. Each verb names its own table.
The read verbs are `get`, `scan` and `count`. The write verbs are `put` and `delete` (values encoded
through the same declared-type contract):
```sh { title='Get one row as JSON, or null' }
iq --src books exec get iq_books 1
```
```sh { title='Scan up to 10 rows as {rowkey: row}' }
iq --src books exec scan iq_books 10
```
```sh { title='Count the rows (a key-only scan)' }
iq --src books exec count iq_books
```
```sh { title='Write one cell' }
iq --src books exec put iq_books 5 cf:title Dune
```
```sh { title='Delete the whole row (add cf:title for one cell)' }
iq --src books exec delete iq_books 5
```
Structured writes go through `iq data` (`clear`/`drop`/`delete`, plus `--insert`) with write modes,
stats and `--explain`. `iq data delete …` is the typed, capability-gated per-key
delete that formalizes the raw `delete` verb above. The raw `put`/`delete` verbs remain the
lower-level raw path (a single cell, a column), mirroring the other drivers' raw paths. `iq
inspect` lists the source namespace's tables (`tables`).
## MongoDB
[MongoDB](https://www.mongodb.com) is a document database that stores
JSON-like documents in collections.
After you register a `mongodb://` source (`mongodb+srv://` for SRV discovery), the same jq interface works against a collection. **The collection is the keyspace, a document's `_id` is the key and the document is the value**.
The database comes from the URI path. The collection comes from the URI's `?collection=` (the driver's own connection option, overridable per run with a dotted
`handle.collection`).
```sh { title='Register a MongoDB source' }
iq add -n books 'mongodb://localhost:27017/iq?collection=books'
```
```sh { title='Fetch the document whose _id is "2"' }
iq --src books '.["2"]'
```
```sh { title='Stream the collection, filtered' }
iq --src books '.[] | select(.year > 2015) | .title'
```
```sh { title='Materialize every _id' }
iq --src books --unbounded 'keys'
```
Because Mongo values are natively typed, numeric comparisons like `.year > 2015` need no
`tonumber`. This is unlike Redis, where everything is a string. Documents normalize to JSON with
the same rules everywhere:
- An `ObjectID` becomes its hex string.
- A date becomes an RFC 3339 string.
- Numbers stay numbers.
- Nested documents and arrays are preserved.
A missing `_id` reads as `null`.
The `--unbounded` / streaming rules are identical to every backend (`.[]`-rooted filters stream a cursor
in constant memory, `keys`/`.`/`map` materialize and require the flag).
### Pushdown
By default, the driver translates the **equality**, **range**, **regex**, **existence**, **length** and
**array** clauses of a `.[] | select(...)` filter into a native Mongo query. As a result, the server does the filtering (and can use an index) before the
documents ever reach iq:
```sh { title='Pushed down: the server filters by author' }
iq --src books '.[] | select(.author == "Robert C. Martin") | .title'
```
```sh { title='Forced client-side with --no-compile' }
iq --src books --no-compile '.[] | select(.author == "Robert C. Martin") | .title'
```
Pushdown never changes results, only speed. The full jq always re-runs client-side over whatever
comes back, so a pushed filter is only ever a conservative pre-filter. Pass `--no-compile` to skip
it and stream the whole collection, filtering entirely client-side. What it can push:
| `select(...)` clause | Pushed | MongoDB translation | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `{a: x}` | number, string, bool, or null literal |
| `.a == 1 or .a == 2` | ✓ | `{a: {$in: [1, 2]}}` | an `or` of equalities on one field |
| `.a > n`, `>=`, `<`, `<=` | ✓ | native op + `$type` guards (an `$or`) | number/string literal. The translation reproduces jq's cross-type order, so the match is never a subset |
| `.a \| test("re")` | ✓ | `{a: {$regex: "re", $options: "is"}}` | portable patterns only (below), and jq's `i` and `m` flags. jq's `m` (dot-matches-newline) maps to PCRE's `s` |
| `has("a")`, `.a \| has("k")` | ✓ | `{a: {$exists: true}}` | exact, key presence, like jq's `has()` |
| `.a \| length == n` | ✓ | `{$size: n}` + `$type` guards (an `$or`) | jq `length` is polymorphic (array/string/object/number), so guards keep it a superset |
| `.a \| any(cond)` | ✓ | `{a: {$elemMatch: cond}}` (an `$or` with an object guard) | an array element satisfying a pushable element predicate. `cond` can combine the rows above |
| `.a != x` | ✓ | `{$or: [{a: {$ne: x}}, {a: {$type: "array"}}]}` | exact negation of equality (the guard keeps arrays, which jq never equates to a scalar) |
| `has("a") \| not` | ✓ | `{a: {$exists: false}}` | exact negation of existence |
| `.a \| any(.f == v) \| not` | ✓ | `{a: {$not: {$elemMatch: …}}}` | no array element matches. The element condition must be exact equality |
| `E1 and E2`, `E1 or E2` | ✓ | `$and` / `$or` of the above | an `and` can push only its pushable parts and drop the rest |
| negated range/regex/`size` | — | — | their filters are supersets and a negated superset is a subset (unrecoverable) |
| `.a > true`, `.a < null` | — | — | a range against bool/null has no clean superset |
| non-portable regex | — | — | engine-specific construct (below) |
| anything else | — | — | runs client-side, as under `--no-compile` |
**Portable regex.** iq's jq is [gojq](https://github.com/itchyny/gojq), which compiles a `test()`
pattern with Go's RE2. MongoDB uses PCRE. A pattern is pushed only when every construct it uses
means the same, or a superset, in both. These constructs qualify:
- Literals
- Anchors (`^` `$`)
- `.`
- Quantifiers (`* + ? {n,m}`)
- Alternation (`|`)
- Groups
- Character classes
- The ASCII `\d` `\w` `\s` `\D` `\W` shorthands
- Word boundaries (`\b`, `\B`).
`\S` is the one shorthand held back. RE2's `\s` omits the vertical tab that PCRE's `\s` matches.
As a result, RE2's `\S` matches a vertical tab that PCRE's does not.
If pushed, it drops a document jq keeps (`\s` diverges the other way, a superset the client-side
re-run corrects).
Flags follow the same rule. gojq accepts only `i`, `m`, `g`, and iq pushes `i`
(case-insensitive) and `m`. In jq, `m` means "`.` matches newline" (dotall), so it maps to PCRE's
`s`, not PCRE's `m`.
A pattern that uses one of these constructs is not portable and stays client-side:
- Lookaround (`(?=…)`)
- Backreferences (`\1`)
- Unicode properties (`\p{…}`)
- POSIX classes (`[[:…:]]`)
- Possessive quantifiers.
As a result, the pushed set always equals jq's.
### Raw commands
`iq exec` runs a single JSON command document with `runCommand` and prints the reply as
JSON. It is the raw path for server-side queries, aggregation and administration:
```sh { title='Run a native find command' }
iq --src books exec '{"find":"books","filter":{"year":{"$gt":2015}}}'
```
```sh { title='Run an aggregation pipeline' }
iq --src books exec '{"aggregate":"books","pipeline":[{"$group":{"_id":null,"avg":{"$avg":"$price"}}}],"cursor":{}}'
```
`iq inspect` runs these diagnostic database commands:
- `dbStats`
- `serverStatus`
- `listCollections`
- `collStats` (needs a collection, address it as `source.collection` or set `?collection=` on the
source URI)
- `buildInfo`
- `hostInfo`.
`--only` narrows to those subcommands.
## Neo4j
[Neo4j](https://neo4j.com) is a graph database of nodes and relationships,
queried with Cypher.
After you register a `neo4j://` source, the same jq interface works against a node label. **The node label is the keyspace, a node's key is the key and the node is the value**.
Neo4j has no single keyspace, so a label is the addressable collection (like a Mongo collection or a
Cassandra table). The host is the bolt server. The label rides in the URI's `?label=` (overridable
per run with a dotted `handle.label`). Neo4j is multi-database, and the database is `?database=`
(default `neo4j`). Use `neo4j+s://` (or `bolt://` for a single instance, `+s`/`+ssc` for TLS).
**Credentials
travel in the URI userinfo** (bolt basic auth), so `--store keyring` moves the password to the OS
keyring exactly as for the other backends.
**The key is the elementId by default, or a property you name with `?key=`.** `elementId(n)` is
always present and unique, but opaque and not stable across database reloads. As a result, a
`?key=` property (a stable, human-meaningful id) reads better. The value carries `_id` (the
elementId) and `_labels` alongside the node's properties, so identity survives whichever key you
choose.
```sh { title='Register a Neo4j source keyed by the "id" property' }
iq add -n graph 'neo4j://neo4j:password@localhost:7687/?label=Person&key=id'
```
```sh { title='Fetch the Person whose id is 1' }
iq --src graph '.["1"]'
```
```sh { title='Stream the label, filtered' }
iq --src graph '.[] | select(.age > 40) | .name'
```
```sh { title='Materialize every key in the label' }
iq --src graph --unbounded 'keys'
```
```sh { title='Query a different label on the same database' }
iq --src graph.Book '.[]'
```
Neo4j values map to JSON directly. Integers keep exact precision. Bytes become base64. Temporal
and spatial values become their canonical ISO strings and `{x,y,srid}` objects.
A scan pages the label with keyset pagination ordered by `elementId(n)`. A `?key=` property is not
guaranteed unique (unlike a primary key). For this reason, a scan falls back to a node's elementId
whenever the key collides within a page, so no node is ever silently dropped. A bounded `.["v"]`
lookup that matches more than one node is an error rather than an arbitrary pick.
### Relationship collections
A **relationship type** is an addressable collection too, so you can query a graph's edges the same
way. Name it with `?rel=KNOWS` on the source, or address one per run with the `:` marker
(`handle.:KNOWS`). A leading colon can never be a valid label, so it unambiguously selects a
relationship type. A source names either a label or a relationship type, not both.
```sh { title='Register a relationship-type source' }
iq add -n edges 'neo4j://neo4j:password@localhost:7687/?rel=WROTE'
```
```sh { title='Stream WROTE edges, filtered (pushdown on the edge)' }
iq --src edges '.[] | select(.year > 2015)'
```
```sh { title='Address the type per run from any source' }
iq --src graph.:WROTE '.[]'
```
Each relationship's value is its properties plus a self-describing envelope: `_type` (the type),
`_id` (its elementId) and `_start` / `_end` (the endpoint node elementIds). The scan, key, count,
and `select(...)` pushdown rules are identical to nodes (the predicate is pushed onto the edge
variable).
**Relationship collections are read-only for now.** Creating an edge needs endpoint
resolution (which nodes to connect and by which key), and that is a further follow-up. As a
result, `iq` refuses a copy or `iq data` write into a relationship source with a clear message.
Write nodes with `?label=`.
### Pushdown
By default, the driver translates the **equality** and **existence** clauses of a
`.[] | select(...)` filter into a Cypher `WHERE` clause. As a result, the server filters before
nodes reach iq. The clause uses dynamic `n[$prop]` access, so the property name is a parameter,
never string-built:
| `select(...)` clause | Pushed | Cypher | Notes |
| --- | :---: | --- | --- |
| `.a == x` | ✓ | `n[$p] = $v` | equality. A `null` literal becomes `n[$p] IS NULL` (a missing property) |
| `.a \| has` / `has("a")` | ✓ | `n[$p] IS NOT NULL` | key presence, exact |
| `has("a") \| not` | ✓ | `n[$p] IS NULL` | key absence, exact |
| `E1 and E2` | ✓ | `(… AND …)` | drops any conjunct it cannot push (widening) |
| `E1 or E2` | ✓ | `(… OR …)` | pushed only when **every** branch is pushable |
| `.a > x` / `.a <= x`, `!=`, `length`, regex, `any`, nested paths | — | — | run client-side. Cypher compares mismatched types as null rather than by jq's cross-type ordering. A nested path has no flat Neo4j property. As a result, pushing these can wrongly exclude a node that jq keeps |
Pushdown never changes results, only speed. The full jq always re-runs client-side, so a pushed
filter is a conservative pre-filter. `--explain` shows the `WHERE` clause. `--no-compile`
streams the whole label and filters entirely client-side.
### Writing
A copy into a Neo4j label upserts each node with `MERGE (n:Label {key}) SET n += props`, so a re-run
converges. **Writing needs a `?key=` property** (a MERGE key must be stable and the elementId is
server-assigned) **and a uniqueness constraint on it** (`CREATE CONSTRAINT ... REQUIRE n. IS
UNIQUE`). Without the constraint, a MERGE can match and overwrite several nodes at once. For this
reason, the write is refused up front rather than fanning out.
`iq data clear` detach-deletes every node in the
label (and the relationships they hold). A label is not a droppable container, so `iq data drop` is
unsupported. Writes set node properties only. Relationships are a follow-up.
### Raw commands
`iq exec` runs raw, parameterized [Cypher](https://neo4j.com/docs/cypher-manual/current/). The first
argument is the statement. An optional second argument is a JSON object of parameters (passed as
parameters, never string-built into the statement). It prints the result rows as JSON:
```sh { title='Run parameterized Cypher' }
iq --src graph exec 'MATCH (n:Person) WHERE n.age > $min RETURN n.name, n.age' '{"min": 40}'
```
```sh { title='Count the nodes' }
iq --src graph exec 'MATCH (n) RETURN count(n) AS nodes'
```
`iq inspect` reads deployment and schema metadata through these subcommands:
- `server` (components and version)
- `databases` (the deployment's databases)
- `labels` (the addressable node labels)
- `reltypes` (relationship types)
- `constraints` (which shows the uniqueness constraint a `?key=` write needs).
`--only` narrows to those subcommands.
## Redis
[Redis](https://redis.io) is an in-memory key-value store used as a cache,
database and message broker.
After you register a `redis://` source (`rediss://` for TLS), the same jq interface works against the
Redis keyspace. **A key maps directly to a Redis key and the value is whatever that key holds**.
The database index comes from the URI path (`/0`). Every value is a string, so numeric
comparisons need `tonumber`.
```sh { title='Register a Redis source' }
iq add -n cache redis://localhost:6379/0
```
```sh { title='Fetch the key "greeting"' }
iq --src cache '.greeting'
```
```sh { title='Stream the keyspace, filtered (string values need tonumber)' }
iq --src cache '.[] | select((.year|tonumber) > 2015) | .title'
```
```sh { title='Materialize every key' }
iq --src cache --unbounded 'keys'
```
The `--unbounded` / streaming rules match every backend (`.[]`-rooted filters stream in constant
memory, `keys`/`.`/`map` materialize and require the flag).
### Value encoding
Each fetched Redis value is normalized to JSON by type:
| Redis type | JSON shape |
| --- | --- |
| string | the string verbatim (numeric strings stay strings, use `tonumber`) |
| hash | object `{field: value}` |
| list | array, in list order |
| set | array, sorted lexically (sets have no native order) |
| sorted set | array of `{"member": ..., "score": ...}`, in ascending score order |
| stream | array of `{"id": ..., "fields": {field: value}}`, in entry order |
| RedisJSON | the stored document, parsed as JSON |
| missing key | `null` |
Other module types (time series, bloom, …) have no frozen encoding yet. `iq` refuses a named
read of one with a clear message.
### Pushdown
Redis has no server-side filtering. As a result, a compiled predicate drives a **client-side
raw-byte prefilter** instead. On a streaming scan, the prefilter tests each RedisJSON value against
the predicate on its raw JSON.GET bytes. When the value provably cannot match, the prefilter drops
it before the (dominant) decode. Every other type is decoded and included unchanged.
The full jq always re-runs client-side, so output is identical with or without the prefilter. The
prefilter only skips decoding documents that the filter rejects. `--no-compile` turns it off.
### Raw commands
`iq exec` forwards a command to the database verbatim and prints the reply in redis-cli style.
It is the raw path for writes, administration and seeding that the jq read path does not cover:
```sh { title='Set a key, replies "OK"' }
iq --src cache exec SET greeting hello
```
```sh { title='Read it back, replies "hello"' }
iq --src cache exec GET greeting
```
```sh { title='Increment a counter, replies (integer) 1' }
iq --src cache exec INCR counter
```
```sh { title='Read a missing key, replies (nil)' }
iq --src cache exec GET missing
```
Its output mirrors redis-cli's cooked style:
- Bulk strings are quoted.
- Integers appear as `(integer) N`.
- A missing value appears as `(nil)`.
- Arrays appear as a numbered, indented list.
The client uses RESP2, so aggregate replies match redis-cli's classic flat output. Status replies
such as `OK` and `PONG` appear quoted. This is a limitation of the underlying client, which does not
distinguish them from bulk strings.
`iq inspect` runs `INFO`. `--only` narrows it to sections (`server`, `clients`, `memory`,
`persistence`, `stats`, `replication`, `cpu`, `keyspace`). With no section, it runs the full `INFO`.
## File dumps
A `file://` source reads a database dump straight from disk. As a result, you can do these
operations on a snapshot with the same jq interface, with **no running server**:
- Query it
- Inspect it for shape
- Diff it
- Restore it.
A `file://` source is read-only. A `file://` endpoint is never a copy *destination*.
`iq exec`/`iq inspect` (which need a live server) do not apply.
```sh { title='Register a dump like any source' }
iq add file:///backups/prod.rdb -n snap
```
```sh { title='Bounded read of one key' }
iq --src snap '.["session:42"]'
```
```sh { title='Streamed scan' }
iq --src snap '.[] | select(.active)'
```
```sh { title='Whole-dataset filters obey --unbounded' }
iq --src snap 'keys' --unbounded
```
```sh { title='Restore the dump into a live source' }
iq --src snap --insert prod
```
```sh { title='Diff a dump against a live source' }
iq diff snap prod --data
```
The format is detected from the file's content (or forced with a `?format=` query, for example
`file:///d.bin?format=bson`). A gzipped dump is unwrapped automatically. A gzipped dump must
pass `?format=`, because its content is not sniffable through the compression. On Windows, a drive
path takes the `file:///C:/path/to/dump.json` form (forward slashes, three slashes before the
drive letter).
| Format | Produced by | Notes |
| --- | --- | --- |
| Typed JSONL | `iq --src --typed -o ` | iq's own dump, lossless round-trip |
| Typed YAML | `iq --src --typed -y -o ` | the same records as YAML documents, auto-detected by a `.yaml`/`.yml` name, else `?format=yaml` |
| Redis RDB | `redis-cli --rdb`, `SAVE` | values match a live scan, RDB ≤ v12 (Redis ≤ 7.2) |
| Mongo BSON | `mongodump` | single `.bson` file |
| Mongo Extended JSON | `mongoexport` | one document per line, or a `--jsonArray` array |
| DynamoDB JSON | S3 `export-table-to-point-in-time`, `aws dynamodb scan` | needs `?format=dynamodb-json` and a `?keys=pk[:S][,sk[:N]]` key schema (a dump carries items but not the table's key schema). Export files are gzipped NDJSON |
| Cassandra CSV | `cqlsh COPY … TO 'f.csv'` | needs `?format=cassandra-csv`, `?keys=col1[,col2]` naming the primary-key columns, and `?types=col=cqltype,…` for the non-text columns (COPY writes every value as text). Column names come from a `WITH HEADER=TRUE` row, else `?columns=`. Scalar columns only |
| Neo4j APOC JSON | `CALL apoc.export.json.all('g.json',{})` | needs `?format=neo4j-json` and a keyspace selector: either `?label=` for its nodes or `?rel=` for its relationships (a dump holds the whole graph). JSON Lines or `ARRAY_JSON`. Same `_id`/`_labels`/`_type`/`_start`/`_end` envelope as the live driver. `?key=` keys by a property. `_id` is APOC's numeric export id, not the live elementId. The binary `neo4j-admin database dump` and APOC's `useTypes`/`JSON_ID_AS_KEYS` variants are not supported |
The whole dump streams. A `file://` source never holds all values in memory (whole-dataset
materialization is the core's, gated by `--unbounded`, exactly as for a live backend).
**Restore fidelity** is the record round-trip. Values and native types reconstruct, but these do
not carry:
- TTLs
- Exact encodings
- Stream consumer groups
- RDB module types
- Mongo indexes.
**Neo4j record ids.** A dump's `_id` and a relationship's `_start`/`_end`, is APOC's numeric
export id, not the live driver's `elementId`. The reason is that APOC's default export does not
write elementIds. Within one dump, the ids are self-consistent. A relationship's `_start`/`_end`
reference the same ids that its nodes carry as `_id`. As a result, `?rel=` endpoints resolve against
the default-keyed `?label=` nodes exactly as they do live.
The numeric id cannot do two things, both by nature:
- It does not match the `_id` of the same node read from the live source (different id schemes).
- It is not stable across re-exports (Neo4j reuses a deleted node's id, the reason `id()` is
deprecated in favor of `elementId()`).
Key on a business property with `?key=`. The result is an identifier that is stable and
identical across a live source and its dump. That property reads the same value everywhere.
**Decode cache.** Re-querying the same large dump re-parses it every time. For this reason, iq
caches the decoded, normalized records of a scanned dump above 4 MiB. It reads them back on later
queries and skips the RDB/BSON/JSON decode (a warm scan of an 8 MiB dump runs several times faster).
The cache lives under `/iq/dumps`. It keys on the dump's path, size and mtime. As a
result, editing the dump invalidates the cache automatically and needs no action from you. A stale
or absent cache only means a full decode, never a wrong or failed query.
Only full scans populate it (a bounded
key read does not) and stdin is never cached.
Alongside the records, a scan writes a **per-page key index** (a Bloom filter per page). As a
result, a later bounded read (`iq --src snap '.["id"]'`) decodes only the pages that can hold a
wanted key, instead of streaming the whole cache. A point lookup or a missing-key check stays fast
even on a huge dump. The index is on by default and distribution-agnostic (it hashes keys, so
random ids/UUIDs are fine). If a very large keyspace makes the index build memory unwelcome, skip it
with `--no-cache-index` (the flat cache is still written, a bounded read only streams it).
Manage the cache with `iq cache`. Bypass it for one run with `--no-cache` :material-earth:{ title="Global flag" }. Set a default
with `iq config set no-cache true` / `iq config set no-cache-index true` (`--no-cache-index` :material-earth:{ title="Global flag" }
too).
- `iq cache location`: print the cache directory path.
- `iq cache stat [-j/--json | -y/--yaml]`: list cached dumps with their sizes.
- `iq cache clear [|]`: remove all cached dumps, or only one source's/path's.
**Prefilter.** A file source pushes no filter to a server (there is none). But on a streaming
scan of an **uncached typed-JSONL** dump, a compiled predicate drives a **client-side raw-byte
prefilter**. The prefilter tests the raw `value` bytes of each record against the predicate. It
drops a provable non-match before it is decoded. As a result, the dominant JSON decode is skipped
for records that the filter rejects.
Every other format (YAML, RDB, BSON, Extended JSON, DynamoDB JSON, Cassandra
CSV, Neo4j APOC) and any scan served from a fresh decode cache (whose bytes are already-decoded
CBOR and already fast to stream), decodes in full and lets the client filter.
The full jq always
re-runs client-side, so output is identical with or without the prefilter. The prefilter only skips
decoding dropped records. A prefiltered scan deliberately does not populate the decode cache (to
populate it, the scan must decode everything). `--no-compile` turns the prefilter off.
# Comparison
docs/docs/comparison.md
# Comparison
How `iq` relates to other query tools. Its niche is narrow: a single static binary that gives
NoSQL stores one jq-based query interface. The filter runs client-side over normalized JSON, so
semantics are identical across backends.
## By job
| Job | What people use today | What `iq` changes |
| --- | --- | --- |
| Query a live store from a shell | `redis-cli`, `mongosh`, `cqlsh`, `aws dynamodb`, `curl` against Elasticsearch, each piped into `jq` | one language and one config over all of them. The filter names the keys. Paging and normalization are handled |
| Inspect a backup | `redis-rdb-tools`, `bsondump`, `mongoexport` files, DynamoDB export JSON, `cqlsh COPY` CSV, APOC JSON, each read by its own tool or by hand | one reader over six formats, queryable, diffable, restorable, with no server |
| Copy or migrate between stores | ad-hoc scripts, `mongodump`/`mongorestore` and `elasticdump` for one store at a time, Redpanda Connect or Bento for any-to-any (a YAML pipeline plus the Bloblang mapping language), Airbyte for a platform | one command, a typed round-trip, an inline jq transform, cross-driver |
| Compare environments, watch schema drift | export both sides, then `diff`, `jd` or `jq` by hand | `iq diff` over data, stats, or inferred schema, with `diff(1)` exit codes for CI |
| Give an AI agent database access | one MCP server per backend (MongoDB's own, Google's MCP Toolbox for Databases), each speaking its native dialect behind a server process | one binary and one language for all of them. `--explain` as a dry run. A skill that any agent that reads the Agent Skills format can install. An MCP server, read-only by default (see [AI agents](agents.md)) |
## What iq is not
- Not an analytics engine. Pushdown differs by backend. Every server-side backend pushes equality,
and all except Cassandra and HBase also push existence. Ranges push on MongoDB, CouchDB and
Couchbase, and regex on MongoDB and CouchDB. Redis and file dumps have no server-side filter, so a
client-side byte prefilter takes its place. Everything else runs
as a client-side scan. An aggregate materializes the keyspace behind `--unbounded`. A heavy
question belongs in the backend's own language through `iq exec`, or in a query engine.
- Not a replacement for the native shell where the backend's own feature is the point:
aggregation pipelines, relevance scoring, graph traversals, vector search. `iq exec` forwards
those verbatim rather than modelling them.
## Tools that unify many databases under one language
Legend: ● primary, ◐ partial, — none. Model is the shape the query language speaks. Footprint is
what you run.
| Tool | Query language | Relational | NoSQL | Files | Data model | Footprint |
|---|---|:---:|:---:|:---:|---|---|
| **iq** | **jq** | — | **●** | **◐** | **document** | **single binary** |
| [sq](https://sq.io) | SLQ / SQL | ● | — | ● | tabular | single binary |
| [usql](https://github.com/xo/usql) | native SQL | ● | ◐ | — | tabular | single binary (multiplexer) |
| [OctoSQL](https://github.com/cube2222/octosql) | SQL | ● | ◐ | ● | tabular | single binary |
| [DuckDB](https://duckdb.org) | SQL | ◐ | — | ● | tabular | in-process / CLI |
| SQL over files ([dsq](https://github.com/multiprocessio/dsq), [trdsql](https://github.com/noborus/trdsql)) | SQL | — | — | ● | tabular | single binary |
| [Trino](https://trino.io) / [Presto](https://prestodb.io) | SQL | ● | ● | ● | tabular (◐ JSON) | server / engine |
| [Apache Drill](https://drill.apache.org) | SQL | ● | ● | ● | schema-free (both) | server / engine |
| Data virtualization ([Denodo](https://www.denodo.com), [Dremio](https://www.dremio.com), [MindsDB](https://mindsdb.com)) | SQL | ● | ● | ◐ | virtual relational | server |
| Universal clients ([DBeaver](https://dbeaver.io), [DataGrip](https://www.jetbrains.com/datagrip/), [DBX](https://github.com/t8y2/dbx), [LazySQL](https://github.com/jorgerojas26/lazysql)) | native per-backend | ● | ◐ | ◐ | client-side, per backend | desktop app / TUI |
| [Redpanda Connect](https://github.com/redpanda-data/connect) | Bloblang, a mapping language | ◐ | ● | ◐ | document | single binary (YAML pipeline) |
| [MCP Toolbox for Databases](https://github.com/googleapis/genai-toolbox) | native per-backend, as MCP tools | ● | ● | — | per backend | server |
Placement is by each tool's primary targets. Several (Trino, Drill, OctoSQL, DuckDB) partially
cover neighbouring columns via connectors or extensions. `iq` covers files the same way: a
read-only `file://` source over database dumps, not arbitrary files.
`sq`, the tool `iq`'s command set is modelled on, unifies relational databases and files,
and never covers NoSQL. Language specs and embedded libraries (PartiQL, SQL++ / N1QL, JSONiq,
Apache Calcite, GraphQL federation) span nested and tabular data too. But they are
specifications or components inside an engine, not something anyone runs instead of a CLI.
The takeaway is the NoSQL column paired with footprint. Among these tools, `iq` is the only
single binary that gives NoSQL stores one query language. What covers more runs as a server
(Trino, Drill, the virtualization platforms, the MCP Toolbox). What is as light either
speaks each backend's own dialect (usql, the universal clients) or targets files and relational
stores instead (sq, DuckDB, dsq). Redpanda Connect is a single binary too, but Bloblang maps
records through a pipeline. It is not a query language you type at a shell.
# See also
docs/docs/see-also.md
# See also
Where `iq` sits among other tools, and the jq ecosystem its filter language carries over.
- [jq manual](https://jqlang.org/manual/): the language reference for the filters `iq` runs.
- [awesome-jq](https://github.com/jqlang/awesome-jq): a curated list of jq tools, guides, and resources.
- [sq](https://github.com/neilotoole/sq): jq-style queries over SQL databases and document files.
- [gojq](https://github.com/itchyny/gojq): the pure-Go jq implementation `iq` embeds.
- [jaq](https://github.com/01mf02/jaq): a Rust jq clone focused on speed and stricter semantics.
- [yq](https://github.com/mikefarah/yq): jq-style filters for YAML, TOML, and XML.
- [fq](https://github.com/wader/fq): jq for binary formats.
- [jc](https://github.com/kellyjonbrazil/jc): converts classic CLI output to JSON.
- [jqp](https://github.com/noahgorstein/jqp): a TUI playground that live-previews a filter as you type.
- [ijq](https://github.com/gpanders/ijq): interactive jq with a side-by-side input and output view.