Skip to content

Commit abaf288

Browse files
authored
docs: refresh README versions and add non-interactive flow demos (#487)
Address the v0.21.0 README follow-ups from the roadmap. - #454: refresh the shell snippet and benchmark caption to the current release, correct the "not supported" list for v0.21.0 (DDL runs in-memory; transaction control, VACUUM, ATTACH/DETACH, and DCL are rejected), and add a Go test that fails when a README "sqly vX.Y.Z" string drifts from the latest CHANGELOG version. - #455: add VHS demos and examples for --inspect (including --inspect-sample 0 for a schema-only report), --stdin combined with --sql-file (via the committed doc/vhs/join.sql), and the write-back safety boundaries (--save requires --force; a schema change is rejected up front). The new example commands are exercised end-to-end by the shellspec suite. Closes #454 Closes #455
1 parent 448a8ab commit abaf288

11 files changed

Lines changed: 181 additions & 13 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## [Unreleased]
4+
5+
### Documentation
6+
* README Version Refresh: Refresh the shell snippet and benchmark caption to the current release, correct the "not supported" list for v0.21.0 (DDL runs in-memory; transaction control, VACUUM, ATTACH/DETACH, and DCL are rejected), and add a Go test that fails when a README `sqly vX.Y.Z` string drifts from the latest CHANGELOG version (#454).
7+
* README Demos For Non-Interactive Flows: Add VHS demos and examples for `--inspect` (including `--inspect-sample 0` for a schema-only report), `--stdin` combined with `--sql-file`, and the write-back safety boundaries (`--save` requires `--force`; a schema change is rejected up front). The new example commands are exercised end-to-end by the shellspec suite (#455).
8+
39
## [v0.21.0](https://github.com/nao1215/sqly/compare/v0.20.0...v0.21.0) (2026-06-01)
410

511
### Breaking Changes

README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Run `sqly` without `--sql` to open the shell. It behaves like `sqlite3` or `mysq
9797

9898
```shell
9999
$ sqly testdata/user.csv
100-
sqly v0.19.0
100+
sqly v0.21.0
101101

102102
enter "SQL query" or "sqly command that begins with a dot".
103103
.help print usage, .exit exit sqly.
@@ -206,11 +206,13 @@ $ printf '.tables\nSELECT COUNT(*) FROM user\n' | sqly testdata/user.csv
206206

207207
`--sql-file PATH` runs SQL read from a file (multiple `;`-separated statements allowed). It cannot be combined with `--sql`. Because the query comes from a file, stdin stays free for a dataset:
208208

209+
![sql-file demo](./doc/img/sql-file-demo.gif)
210+
209211
```shell
210-
$ cat testdata/user.csv | sqly --stdin csv --sql-file join.sql testdata/identifier.csv
212+
$ cat testdata/user.csv | sqly --stdin csv --sql-file doc/vhs/join.sql testdata/identifier.csv
211213
```
212214

213-
where `join.sql` holds:
215+
where `doc/vhs/join.sql` holds:
214216

215217
```sql
216218
SELECT s.user_name, i.position
@@ -223,6 +225,8 @@ ORDER BY s.identifier;
223225

224226
`--inspect` imports the inputs, prints a JSON report of every table (name, source, columns, row count, sample rows), and exits without the shell. It is the non-interactive equivalent of `.tables` + `.schema` + `.describe`, useful for scripts and LLMs. Import progress goes to stderr, so stdout is JSON only. `--inspect-sample N` sets the sample size (default 5; `0` for schema only).
225227

228+
![inspect demo](./doc/img/inspect-demo.gif)
229+
226230
```shell
227231
$ sqly --inspect --inspect-sample 1 testdata/identifier.csv
228232
{
@@ -243,6 +247,12 @@ $ sqly --inspect --inspect-sample 1 testdata/identifier.csv
243247
}
244248
```
245249

250+
Use `--inspect-sample 0` for a schema-only report (no `sample_rows`), so a script can read column types without pulling any data:
251+
252+
```shell
253+
$ sqly --inspect --inspect-sample 0 testdata/identifier.csv
254+
```
255+
246256
## Inspect schema: .schema and .describe
247257

248258
```shell
@@ -271,6 +281,17 @@ $ sqly --sql "UPDATE user SET first_name = 'Rachelle' WHERE identifier = 1" --sa
271281
$ sqly --sql "DELETE FROM user WHERE identifier > 100" --save --force testdata/user.csv
272282
```
273283

284+
Write-back is deliberately strict about what it will touch. `--save` refuses to run without `--force`, and a run whose SQL changes schema (DDL such as `CREATE TABLE ... AS SELECT`) is rejected up front, before any output is written, since only row changes map back to a file:
285+
286+
![write-back safety demo](./doc/img/writeback-demo.gif)
287+
288+
```shell
289+
$ sqly --sql "UPDATE user SET identifier = identifier + 100" --save testdata/user.csv
290+
--save overwrites source files; pass --force to confirm, or use --save-dir DIR to write elsewhere
291+
$ sqly --sql "CREATE TABLE backup AS SELECT * FROM user" --save-dir ./out testdata/user.csv
292+
--save/--save-dir cannot persist "CREATE TABLE backup AS SELECT * FROM user": ... only INSERT/UPDATE/DELETE on imported tables are saved
293+
```
294+
274295
Only tables mapping one-to-one to a single CSV, TSV, LTSV, or Parquet source can be written. Tables created by SQL, directory imports, and multi-table sources (Excel, ACH, Fedwire) are rejected with a clear error before anything is written.
275296

276297
## Directory import
@@ -379,7 +400,7 @@ SELECT * FROM `customers100000` WHERE `Index` BETWEEN 1000 AND 2000 ORDER BY `In
379400
|--------:|--------:|------------:|--------------:|-------------------:|
380401
| 100,000 | 12 | 515 ms | 161 MB | 2.82M |
381402

382-
Measured on an AMD Ryzen 7 5800U, Go 1.25, sqly v0.19.0. Run `make bench` to reproduce on your machine.
403+
Measured on an AMD Ryzen 7 5800U, Go 1.25, sqly v0.21.0. Run `make bench` to reproduce on your machine.
383404

384405
## Comparison with similar tools
385406

@@ -406,9 +427,13 @@ sqly stays in the same sub-second range as the CSV-focused tools while reading t
406427

407428
## Limitations (not supported)
408429

409-
- DDL such as CREATE
410-
- DML such as GRANT
411-
- TCL such as transactions
430+
sqly runs each statement in its own transaction on an in-memory database, so a few SQLite statements are rejected with a clear error rather than failing in confusing ways:
431+
432+
- Explicit transaction control: `BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `RELEASE`
433+
- `VACUUM` / `VACUUM INTO`, and `ATTACH` / `DETACH DATABASE`
434+
- DCL such as `GRANT` / `REVOKE`
435+
436+
DDL (`CREATE`, `DROP`, `ALTER`, ...) runs against the in-memory tables, but a non-interactive `--save`/`--save-dir` run rejects a schema change up front, since only `INSERT`/`UPDATE`/`DELETE` on an imported table can be written back to a file.
412437

413438
## Contributing
414439

doc/img/inspect-demo.gif

199 KB
Loading

doc/img/sql-file-demo.gif

51.1 KB
Loading

doc/img/writeback-demo.gif

164 KB
Loading

doc/vhs/inspect.tape

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# --inspect demo: machine-readable JSON report, with and without sample rows.
2+
# Regenerate with: vhs doc/vhs/inspect.tape (run from the repository root)
3+
Output "doc/img/inspect-demo.gif"
4+
Set Shell "bash"
5+
Set FontSize 16
6+
Set Width 1100
7+
Set Height 560
8+
Set Padding 18
9+
Hide
10+
Type "export PATH=$PWD:$PATH PS1='$ '; clear"
11+
Enter
12+
Show
13+
Type "sqly --inspect --inspect-sample 1 testdata/identifier.csv"
14+
Sleep 600ms
15+
Enter
16+
Sleep 3s
17+
Type "sqly --inspect --inspect-sample 0 testdata/identifier.csv"
18+
Sleep 600ms
19+
Enter
20+
Sleep 3s

doc/vhs/join.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Join a piped --stdin dataset (table "stdin") with a file argument.
2+
-- Run with: cat user.csv | sqly --stdin csv --sql-file doc/vhs/join.sql identifier.csv
3+
SELECT s.user_name, i.position
4+
FROM stdin s
5+
JOIN identifier i ON s.identifier = i.id
6+
ORDER BY s.identifier;

doc/vhs/sql_file.tape

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# --stdin + --sql-file demo: piped dataset queried by a script read from a file.
2+
# Regenerate with: vhs doc/vhs/sql_file.tape (run from the repository root)
3+
Output "doc/img/sql-file-demo.gif"
4+
Set Shell "bash"
5+
Set FontSize 16
6+
Set Width 1100
7+
Set Height 320
8+
Set Padding 18
9+
Hide
10+
Type "export PATH=$PWD:$PATH PS1='$ '; clear"
11+
Enter
12+
Show
13+
Type "cat testdata/user.csv | sqly --stdin csv --sql-file doc/vhs/join.sql testdata/identifier.csv"
14+
Sleep 600ms
15+
Enter
16+
Sleep 3s

doc/vhs/writeback.tape

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Write-back safety demo: --save needs --force, schema changes are rejected,
2+
# and --save --force overwrites the source in place.
3+
# Regenerate with: vhs doc/vhs/writeback.tape (run from the repository root)
4+
Output "doc/img/writeback-demo.gif"
5+
Set Shell "bash"
6+
Set FontSize 16
7+
Set Width 1180
8+
Set Height 460
9+
Set Padding 18
10+
Hide
11+
Type "WORK=$(mktemp -d); cp testdata/user.csv $WORK/; export PATH=$PWD:$PATH; cd $WORK; export PS1='$ '; clear"
12+
Enter
13+
Show
14+
Type "sqly --sql 'UPDATE user SET identifier = identifier + 100' --save user.csv"
15+
Sleep 600ms
16+
Enter
17+
Sleep 2500ms
18+
Type "sqly --sql 'CREATE TABLE backup AS SELECT * FROM user' --save-dir ./out user.csv"
19+
Sleep 600ms
20+
Enter
21+
Sleep 2500ms
22+
Type "sqly --sql 'UPDATE user SET identifier = identifier + 100' --save --force user.csv"
23+
Sleep 600ms
24+
Enter
25+
Sleep 2s
26+
Type "head -2 user.csv"
27+
Sleep 600ms
28+
Enter
29+
Sleep 2s

readme_version_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"regexp"
6+
"testing"
7+
)
8+
9+
// TestREADMEVersionMatchesChangelog guards against release-era version drift in
10+
// the README: every "sqly vX.Y.Z" string (the shell welcome snippet and the
11+
// benchmark caption) must match the latest version heading in CHANGELOG.md. When a
12+
// release bumps the changelog, this fails until the README is refreshed too, so
13+
// stale version strings cannot silently linger. Ref #454.
14+
func TestREADMEVersionMatchesChangelog(t *testing.T) {
15+
t.Parallel()
16+
17+
changelog, err := os.ReadFile("CHANGELOG.md")
18+
if err != nil {
19+
t.Fatalf("read CHANGELOG.md: %v", err)
20+
}
21+
// The first "## [vX.Y.Z]" heading is the latest released version.
22+
headingRe := regexp.MustCompile(`(?m)^## \[(v\d+\.\d+\.\d+)\]`)
23+
heading := headingRe.FindSubmatch(changelog)
24+
if heading == nil {
25+
t.Fatal("no version heading found in CHANGELOG.md")
26+
}
27+
latest := string(heading[1])
28+
29+
readme, err := os.ReadFile("README.md")
30+
if err != nil {
31+
t.Fatalf("read README.md: %v", err)
32+
}
33+
// Match the version that trails an explicit "sqly v" reference, which is what a
34+
// release bump must keep current. Other incidental version strings are ignored.
35+
versionRe := regexp.MustCompile(`sqly (v\d+\.\d+\.\d+)`)
36+
matches := versionRe.FindAllStringSubmatch(string(readme), -1)
37+
if len(matches) == 0 {
38+
t.Fatal(`no "sqly vX.Y.Z" reference found in README.md`)
39+
}
40+
for _, m := range matches {
41+
if m[1] != latest {
42+
t.Errorf("README.md has %q but the latest CHANGELOG version is %q; refresh the README version strings", m[0], latest)
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)