Command line & CI
elyrasql-cli is the client without its window. It does three jobs, for CI
or any script:
checkruns your data-quality checks: queries that return the rows breaking a rule, and pass when they return none.preflightreads migration files and asks the database what each statement would do, without running it. It reports how many rows anUPDATEorDELETEtouches, and whether anALTERthat adds a rule would fail on the rows already there, or (on ElyraSQL) succeed and leave them breaking it.runruns runbooks: it prints each block's result and judges the blocks that say what they should return.
All three only read. The connection is opened read-only, so the server refuses any write, whatever the files hold.
Requires Pro for remote servers
As in the app, a remote connection needs Pro. A local server
(127.0.0.1, localhost) and SQLite files are free. In CI, put your
license key in the ELYRASQL_LICENSE_KEY secret. It is checked online
for that run only and stored nowhere, and every run with the same key
counts as one device.
Install
Each release publishes elyrasql-cli beside the app, at
https://elyracode.com/downloads/sql-client/:
| File | For |
|---|---|
elyrasql-cli-VERSION-linux-x86_64.tar.gz |
Linux x86_64, such as GitHub's ubuntu-latest |
elyrasql-cli-VERSION-linux-aarch64.tar.gz |
Linux on ARM |
elyrasql-cli-VERSION-macos-arm64.tar.gz |
macOS on Apple silicon |
Each has a .sha256 file beside it. The macOS binary is signed and
notarized by Apple, so Gatekeeper lets it run when it was downloaded in a
browser. On Linux, OpenSSL and D-Bus are built in and it needs only glibc
2.34 or later, so it runs on a bare CI image: ubuntu-latest,
ubuntu:22.04, debian:bookworm-slim. To build it yourself instead:
cargo build --release --bin elyrasql-cli --no-default-features --features vendored
Connecting
Give a URL with --url, or set ELYRASQL_URL. Put the password in
ELYRASQL_PASSWORD rather than in the URL.
| Database | URL |
|---|---|
| MySQL | mysql://ci@db.staging:3306/shop |
| MariaDB | mariadb://ci@db.staging:3306/shop |
| ElyraSQL | elyrasql://ci@db.staging:3307/elyra |
| ClickHouse | clickhouse://default@ch.staging:8123/analytics (checks only) |
| SQLite | sqlite:///path/to/file.db or sqlite:relative.db |
Add ?tls=true to require TLS, and --timeout 60 to change the limit per
statement (default 30 s).
On your own machine, --connection staging uses a connection saved in
ElyraSQL Client, with its password from the keychain. elyrasql-cli connections lists them.
Checks
Keep checks as .sql files in the repository. Every statement is a check.
Name each one with a -- @name: line above it:
-- @name: Every order has a customer
SELECT o.id FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE c.id IS NULL;
-- @name: No negative totals
SELECT id, total FROM orders WHERE total < 0;
elyrasql-cli check --url "$STAGING_URL" checks/
A check that returns rows fails, and the first of those rows are printed.
A check can say otherwise with -- @expect: — rows > 0, value >= 100,
last_run > '2026-01-01'; see Runbooks. A directive
that cannot be read stops its check with exit 2, so a typo never passes
silently.
Only reads are run: anything else in a check file is refused and reported,
never run. --saved also runs the checks saved in ElyraSQL Client for that
connection (with --connection).
Migration preflight
elyrasql-cli preflight --url "$STAGING_URL" migrations/
Files are read in name order, and *.down.sql files are skipped unless you
pass --include-down. For each statement, preflight reports:
NOT NULL, a shorterVARCHAR, a narrower integer,UNIQUE, a primary or foreign key (ALTER TABLE …,CREATE UNIQUE INDEX): how many rows break the rule now. It then says whether the statement will fail, silently change rows, or leave them breaking it. This is the same check the app runs before anALTER; see Will this ALTER fail on the rows already there?. Any of the three outcomes fails preflight.UPDATE,DELETE,TRUNCATE,DROP TABLE: how many rows it touches. Every row (noWHERE) is a warning. With--max-rows N, more thanNrows fails.
Each statement is checked against the database as it is now, not as earlier statements in the same files would leave it. A statement on a table that an earlier migration creates is reported as not checked. Triggers, procedures and views are skipped: they have no rows to count.
Runbooks
elyrasql-cli run --url "$STAGING_URL" runbooks/checkout.elyra.sql
Every block of the runbook runs, top to bottom, and its first
rows are printed. A block with -- @expect: passes or fails by it; one
without just shows its result. A block that writes is skipped and reported,
never run. In GitHub format only failures and skipped blocks are annotated,
so a dashboard block does not clutter the pull request.
Output and exit status
| Exit | Means |
|---|---|
| 0 | Everything passed |
| 1 | A check returned rows, an @expect did not hold, or a migration would fail or break rows |
| 2 | Something could not be run: a bad argument, no connection, an unreadable file, a check the server refused |
--format text is the default. --format json is for other tools.
--format github writes annotations on the files and lines in the pull
request, plus a summary table for the job. It is the default when
GITHUB_ACTIONS=true.
GitHub Actions
jobs:
database:
runs-on: ubuntu-latest
env:
ELYRASQL_CLI_VERSION: "0.19.0" # pin the release to use
steps:
- uses: actions/checkout@v4
- name: Install elyrasql-cli
run: |
base=https://elyracode.com/downloads/sql-client
file=elyrasql-cli-${ELYRASQL_CLI_VERSION}-linux-x86_64.tar.gz
curl -fsSLO "$base/$file" -O "$base/$file.sha256"
sha256sum -c "$file.sha256"
sudo tar xzf "$file" --strip-components=1 -C /usr/local/bin --wildcards '*/elyrasql-cli'
- name: Data checks on staging
run: elyrasql-cli check checks/
env:
ELYRASQL_URL: mysql://ci@db.staging:3306/shop
ELYRASQL_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
ELYRASQL_LICENSE_KEY: ${{ secrets.ELYRASQL_LICENSE_KEY }}
- name: Preflight the new migrations
run: elyrasql-cli preflight --max-rows 100000 migrations/
env:
ELYRASQL_URL: mysql://ci@db.staging:3306/shop
ELYRASQL_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
ELYRASQL_LICENSE_KEY: ${{ secrets.ELYRASQL_LICENSE_KEY }}
Use a database user that can read the tables. PROCESS is not needed. A user
with no write grants at all is the safest choice, and enough.