@chrismo/superkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +29 -0
- package/README.md +26 -0
- package/dist/cli/pager.d.ts +6 -0
- package/dist/cli/pager.d.ts.map +1 -0
- package/dist/cli/pager.js +21 -0
- package/dist/cli/pager.js.map +1 -0
- package/dist/cli/skdoc.d.ts +3 -0
- package/dist/cli/skdoc.d.ts.map +1 -0
- package/dist/cli/skdoc.js +42 -0
- package/dist/cli/skdoc.js.map +1 -0
- package/dist/cli/skgrok.d.ts +3 -0
- package/dist/cli/skgrok.d.ts.map +1 -0
- package/dist/cli/skgrok.js +21 -0
- package/dist/cli/skgrok.js.map +1 -0
- package/dist/cli/skops.d.ts +3 -0
- package/dist/cli/skops.d.ts.map +1 -0
- package/dist/cli/skops.js +32 -0
- package/dist/cli/skops.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/docs.d.ts +11 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +29 -0
- package/dist/lib/docs.js.map +1 -0
- package/dist/lib/expert-sections.d.ts +32 -0
- package/dist/lib/expert-sections.d.ts.map +1 -0
- package/dist/lib/expert-sections.js +130 -0
- package/dist/lib/expert-sections.js.map +1 -0
- package/dist/lib/grok.d.ts +15 -0
- package/dist/lib/grok.d.ts.map +1 -0
- package/dist/lib/grok.js +57 -0
- package/dist/lib/grok.js.map +1 -0
- package/dist/lib/help.d.ts +20 -0
- package/dist/lib/help.d.ts.map +1 -0
- package/dist/lib/help.js +163 -0
- package/dist/lib/help.js.map +1 -0
- package/dist/lib/recipes.d.ts +29 -0
- package/dist/lib/recipes.d.ts.map +1 -0
- package/dist/lib/recipes.js +133 -0
- package/dist/lib/recipes.js.map +1 -0
- package/dist/superkit.tar.gz +0 -0
- package/docs/grok-patterns.sup +89 -0
- package/docs/recipes/array.md +66 -0
- package/docs/recipes/array.spq +31 -0
- package/docs/recipes/character.md +110 -0
- package/docs/recipes/character.spq +57 -0
- package/docs/recipes/escape.md +159 -0
- package/docs/recipes/escape.spq +102 -0
- package/docs/recipes/format.md +51 -0
- package/docs/recipes/format.spq +24 -0
- package/docs/recipes/index.md +23 -0
- package/docs/recipes/integer.md +101 -0
- package/docs/recipes/integer.spq +53 -0
- package/docs/recipes/records.md +84 -0
- package/docs/recipes/records.spq +61 -0
- package/docs/recipes/string.md +177 -0
- package/docs/recipes/string.spq +105 -0
- package/docs/superdb-expert.md +929 -0
- package/docs/tutorials/bash_to_sup.md +123 -0
- package/docs/tutorials/chess-tiebreaks.md +233 -0
- package/docs/tutorials/debug.md +439 -0
- package/docs/tutorials/fork_for_window.md +296 -0
- package/docs/tutorials/grok.md +166 -0
- package/docs/tutorials/index.md +10 -0
- package/docs/tutorials/joins.md +79 -0
- package/docs/tutorials/moar_subqueries.md +35 -0
- package/docs/tutorials/subqueries.md +236 -0
- package/docs/tutorials/sup_to_bash.md +164 -0
- package/docs/tutorials/super_db_update.md +34 -0
- package/docs/tutorials/unnest.md +113 -0
- package/docs/zq-to-super-upgrades.md +549 -0
- package/package.json +46 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Getting Bash Text into SuperDB"
|
|
3
|
+
name: bash-to-sup
|
|
4
|
+
description: "Getting raw text safely from Bash into SuperDB without manual escaping."
|
|
5
|
+
layout: default
|
|
6
|
+
nav_order: 1
|
|
7
|
+
parent: Tutorials
|
|
8
|
+
superdb_version: "0.3.0"
|
|
9
|
+
last_updated: "2026-02-17"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Getting Bash Text into SuperDB
|
|
13
|
+
|
|
14
|
+
The companion to [sup_to_bash](sup_to_bash.md), this covers the reverse: safely
|
|
15
|
+
getting raw text from Bash into SuperDB.
|
|
16
|
+
|
|
17
|
+
## The Problem
|
|
18
|
+
|
|
19
|
+
When building `.sup` records from Bash, you need to escape text before embedding
|
|
20
|
+
it in SUP strings. A common approach is manual escaping with sed:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Manual escaping — fragile and error-prone
|
|
24
|
+
escape_for_sup() {
|
|
25
|
+
echo "$1" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed "s/'/\'/g"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
text='She said "hello" and it'\''s a back\slash'
|
|
29
|
+
record="{body:\"$(escape_for_sup "$text")\"}"
|
|
30
|
+
echo "$record" >> data.sup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This works but is brittle — each special character needs its own sed pass, and
|
|
34
|
+
it's easy to miss edge cases or get the escaping order wrong.
|
|
35
|
+
|
|
36
|
+
## Let Super Handle It
|
|
37
|
+
|
|
38
|
+
Instead of escaping text yourself, pipe it through `super` and let the
|
|
39
|
+
serializer do the work. The `-i line` flag reads each line of stdin as a string
|
|
40
|
+
value.
|
|
41
|
+
|
|
42
|
+
### Single-Line Text
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
text='She said "hello" and it'\''s a back\slash'
|
|
46
|
+
echo "$text" | super -s -i line -c "values this" -
|
|
47
|
+
# => "She said \"hello\" and it's a back\\slash"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Super's SUP output automatically escapes backslashes and double quotes. No sed
|
|
51
|
+
needed.
|
|
52
|
+
|
|
53
|
+
### Multiline Text to a Single String
|
|
54
|
+
|
|
55
|
+
With `-i line`, each input line becomes a separate string value. To collapse
|
|
56
|
+
multiline text into a single string with embedded `\n`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
printf 'line one\nline two\nline three' |
|
|
60
|
+
super -s -i line -c 'aggregate s:=collect(this) | values join(s, "\n")' -
|
|
61
|
+
# => "line one\nline two\nline three"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The pattern: `collect()` gathers all lines into an array, then `join()` merges
|
|
65
|
+
them with literal newline characters.
|
|
66
|
+
|
|
67
|
+
### Building Records with Raw Text
|
|
68
|
+
|
|
69
|
+
Combine `-i line` with record construction to safely embed text into `.sup`
|
|
70
|
+
records:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Single-line field
|
|
74
|
+
msg='User said "goodbye" & left'
|
|
75
|
+
body=$(echo "$msg" | super -f line -i line -c "values this" -)
|
|
76
|
+
echo "{type:'message',body:'$body'}" | super -s -c "values this" -
|
|
77
|
+
|
|
78
|
+
# Or build the whole record in one shot
|
|
79
|
+
echo "$msg" | super -s -i line -c "values {type:'message', body: this}" -
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The second form is cleaner — super constructs the full record, so the text never
|
|
83
|
+
passes through Bash string interpolation at all.
|
|
84
|
+
|
|
85
|
+
### Multiline Record Fields
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
notes="first line
|
|
89
|
+
second line
|
|
90
|
+
third line"
|
|
91
|
+
|
|
92
|
+
echo "$notes" |
|
|
93
|
+
super -s -i line -c '
|
|
94
|
+
aggregate s:=collect(this)
|
|
95
|
+
| values {type: "note", body: join(s, "\n")}
|
|
96
|
+
' -
|
|
97
|
+
# => {type:"note",body:"first line\nsecond line\nthird line"}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Appending to .sup Files
|
|
101
|
+
|
|
102
|
+
A common pattern in scripts that maintain `.sup` files:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
append_record() {
|
|
106
|
+
local file="$1"
|
|
107
|
+
local msg="$2"
|
|
108
|
+
echo "$msg" |
|
|
109
|
+
super -s -i line -c "values {ts: now(), body: this}" - >> "$file"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
append_record "log.sup" 'User clicked "submit" at /path?q=1&r=2'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Why This Is Better
|
|
116
|
+
|
|
117
|
+
| Approach | Handles `"` | Handles `\` | Handles `'` | Handles newlines | Handles unicode |
|
|
118
|
+
|---|---|---|---|---|---|
|
|
119
|
+
| Manual sed chains | One sed per char | Easy to mis-order | Another sed | Yet another sed | Hope for the best |
|
|
120
|
+
| `super -i line` | Yes | Yes | Yes | With collect/join | Yes |
|
|
121
|
+
|
|
122
|
+
One pipeline replaces an entire family of escape functions. Super knows its own
|
|
123
|
+
serialization format — let it do the work.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Chess Tiebreaks"
|
|
3
|
+
name: chess-tiebreaks
|
|
4
|
+
description: "End-to-end tutorial parsing PGN chess data to find tie-break games."
|
|
5
|
+
layout: default
|
|
6
|
+
nav_order: 2
|
|
7
|
+
parent: Tutorials
|
|
8
|
+
superdb_version: "0.3.0"
|
|
9
|
+
last_updated: "2026-02-15"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Chess Tiebreaks
|
|
13
|
+
|
|
14
|
+
SuperDB isn't limited to JSON — it can ingest and transform plain text just as
|
|
15
|
+
easily. This tutorial demonstrates parsing raw text input using `grok` patterns,
|
|
16
|
+
reshaping records, and aggregating results to answer a real question.
|
|
17
|
+
|
|
18
|
+
PGN (Portable Game Notation) is a plain text format for recording chess games.
|
|
19
|
+
Each game has metadata in bracketed lines like `[White "LastName, FirstName"]`
|
|
20
|
+
followed by the moves. Here we'll parse a tournament's PGN file to find players
|
|
21
|
+
who faced each other more than once (indicating tie-break games).
|
|
22
|
+
|
|
23
|
+
The data comes from the Tata Steel Masters 2024 broadcast on Lichess, the
|
|
24
|
+
[tie-break games can be seen
|
|
25
|
+
here](https://lichess.org/broadcast/tata-steel-masters-2024/tiebreaks/L43YRQWv#boards).
|
|
26
|
+
|
|
27
|
+
## The Complete Solution
|
|
28
|
+
|
|
29
|
+
```mdtest-command
|
|
30
|
+
curl -sS https://lichess.org/api/broadcast/ycy5D2r8.pgn |
|
|
31
|
+
super -i line -s -c "
|
|
32
|
+
--
|
|
33
|
+
-- parse the pgn file
|
|
34
|
+
--
|
|
35
|
+
where this != ''
|
|
36
|
+
| grok('.*White \"%{WORD:last_name_white}|.*Black \"%{WORD:last_name_black}', this)
|
|
37
|
+
| where not is_error(this)
|
|
38
|
+
|
|
39
|
+
--
|
|
40
|
+
-- pair up each player in each game
|
|
41
|
+
--
|
|
42
|
+
| count
|
|
43
|
+
| put game_id:=((count - 1) / 2)::int64
|
|
44
|
+
| values {...that, game_id}
|
|
45
|
+
| aggregate
|
|
46
|
+
last_name_white:=max(last_name_white),
|
|
47
|
+
last_name_black:=max(last_name_black)
|
|
48
|
+
by game_id
|
|
49
|
+
| drop game_id
|
|
50
|
+
|
|
51
|
+
--
|
|
52
|
+
-- re-organize the data by player and opponent regardless of piece color
|
|
53
|
+
--
|
|
54
|
+
| [
|
|
55
|
+
{player: last_name_white, opponent: last_name_black},
|
|
56
|
+
{player: last_name_black, opponent: last_name_white}
|
|
57
|
+
]
|
|
58
|
+
| unnest this
|
|
59
|
+
|
|
60
|
+
--
|
|
61
|
+
-- count each match-up of player and opponent and find any with counts > 1
|
|
62
|
+
--
|
|
63
|
+
-- this should be players who participated in tie-breaks, as the regular
|
|
64
|
+
-- tournament was only a single round-robin
|
|
65
|
+
--
|
|
66
|
+
| count(this) by player, opponent
|
|
67
|
+
| where count > 1
|
|
68
|
+
| sort player, opponent
|
|
69
|
+
" -
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```mdtest-output
|
|
73
|
+
{player:"Abdusattorov",opponent:"Wei",count:3}
|
|
74
|
+
{player:"Giri",opponent:"Gukesh",count:4}
|
|
75
|
+
{player:"Gukesh",opponent:"Giri",count:4}
|
|
76
|
+
{player:"Gukesh",opponent:"Wei",count:3}
|
|
77
|
+
{player:"Wei",opponent:"Abdusattorov",count:3}
|
|
78
|
+
{player:"Wei",opponent:"Gukesh",count:3}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Walkthrough
|
|
82
|
+
|
|
83
|
+
### Line Input and Filtering
|
|
84
|
+
|
|
85
|
+
`-i line` tells super to treat each line as a separate string record. We filter
|
|
86
|
+
out empty lines with `where this != ''`.
|
|
87
|
+
|
|
88
|
+
### Grok Parsing
|
|
89
|
+
|
|
90
|
+
The grok pattern `'.*White \"%{WORD:last_name_white}|.*Black \"%{WORD:last_name_black}'`
|
|
91
|
+
uses alternation (`|`) to match either a White or Black player line. The
|
|
92
|
+
`%{WORD:...}` capture extracts just the last name (the first word after the
|
|
93
|
+
opening quote).
|
|
94
|
+
|
|
95
|
+
Lines that don't match (like move notation or other metadata) produce errors,
|
|
96
|
+
which we filter with `where not is_error(this)`.
|
|
97
|
+
|
|
98
|
+
### Pairing Records
|
|
99
|
+
|
|
100
|
+
PGN files list White and Black players on consecutive lines for each game. We
|
|
101
|
+
need to combine them into single records.
|
|
102
|
+
|
|
103
|
+
The `count` operator adds a sequential `count` field to each record (1, 2, 3, ...).
|
|
104
|
+
Integer division `(count - 1) / 2` maps pairs of rows to the same game_id:
|
|
105
|
+
rows 1,2 get game_id 0; rows 3,4 get game_id 1, etc. The `count` operator wraps
|
|
106
|
+
the input in a `that` field, so `{...that, game_id}` spreads the original fields
|
|
107
|
+
back out alongside the game_id.
|
|
108
|
+
|
|
109
|
+
Then `aggregate ... by game_id` with `max()` picks up the non-null value from
|
|
110
|
+
each field within each pair.
|
|
111
|
+
|
|
112
|
+
### Reshaping with Arrays and Unnest
|
|
113
|
+
|
|
114
|
+
Each game record has `{last_name_white, last_name_black}`. To count matchups
|
|
115
|
+
regardless of color, we create an array with both perspectives:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
| [{player: last_name_white, opponent: last_name_black},
|
|
119
|
+
{player: last_name_black, opponent: last_name_white}]
|
|
120
|
+
| unnest this
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This doubles our records — each game now appears twice, once from each player's
|
|
124
|
+
perspective.
|
|
125
|
+
|
|
126
|
+
### Final Aggregation
|
|
127
|
+
|
|
128
|
+
`count(this) by player, opponent` counts how many times each matchup occurred.
|
|
129
|
+
`where count > 1` filters to only matchups that happened more than once -
|
|
130
|
+
these are the tie-break games.
|
|
131
|
+
|
|
132
|
+
## Nested `unnest ... into` Example
|
|
133
|
+
|
|
134
|
+
Here's a minimal dataset to demonstrate this next technique, based on similar
|
|
135
|
+
data we were just working with. Four players in a single round-robin (6 games),
|
|
136
|
+
plus one tie-breaker between Gukesh and Wei:
|
|
137
|
+
|
|
138
|
+
```mdtest-input pairings.sup
|
|
139
|
+
{pairing:"Giri Gukesh"}
|
|
140
|
+
{pairing:"Giri Wei"}
|
|
141
|
+
{pairing:"Giri Abdusattorov"}
|
|
142
|
+
{pairing:"Gukesh Wei"}
|
|
143
|
+
{pairing:"Gukesh Abdusattorov"}
|
|
144
|
+
{pairing:"Wei Abdusattorov"}
|
|
145
|
+
{pairing:"Gukesh Wei"}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```mdtest-command
|
|
149
|
+
super -s -c "
|
|
150
|
+
split(pairing, ' ')
|
|
151
|
+
| unnest {pairing:this, player:this} into (
|
|
152
|
+
unnest {player: this.player, opponent: pairing} into (
|
|
153
|
+
where player != opponent
|
|
154
|
+
| cut player, opponent
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
" pairings.sup
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```mdtest-output
|
|
161
|
+
{player:"Giri",opponent:"Gukesh"}
|
|
162
|
+
{player:"Gukesh",opponent:"Giri"}
|
|
163
|
+
{player:"Giri",opponent:"Wei"}
|
|
164
|
+
{player:"Wei",opponent:"Giri"}
|
|
165
|
+
{player:"Giri",opponent:"Abdusattorov"}
|
|
166
|
+
{player:"Abdusattorov",opponent:"Giri"}
|
|
167
|
+
{player:"Gukesh",opponent:"Wei"}
|
|
168
|
+
{player:"Wei",opponent:"Gukesh"}
|
|
169
|
+
{player:"Gukesh",opponent:"Abdusattorov"}
|
|
170
|
+
{player:"Abdusattorov",opponent:"Gukesh"}
|
|
171
|
+
{player:"Wei",opponent:"Abdusattorov"}
|
|
172
|
+
{player:"Abdusattorov",opponent:"Wei"}
|
|
173
|
+
{player:"Gukesh",opponent:"Wei"}
|
|
174
|
+
{player:"Wei",opponent:"Gukesh"}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### How Nested Unnest Works
|
|
178
|
+
|
|
179
|
+
The `unnest ... into` syntax is powerful but takes some unpacking:
|
|
180
|
+
|
|
181
|
+
1. **`split(pairing, ' ')`** turns `"Giri Gukesh"` into `["Giri", "Gukesh"]`
|
|
182
|
+
|
|
183
|
+
2. **First unnest**: `unnest {pairing:this, player:this} into (...)`
|
|
184
|
+
- Keeps the full array as `pairing`
|
|
185
|
+
- Unnests each element as `player`
|
|
186
|
+
- For `["Giri", "Gukesh"]` this produces two records:
|
|
187
|
+
- `{pairing: ["Giri", "Gukesh"], player: "Giri"}`
|
|
188
|
+
- `{pairing: ["Giri", "Gukesh"], player: "Gukesh"}`
|
|
189
|
+
|
|
190
|
+
3. **Second unnest**: `unnest {player: this.player, opponent: pairing} into (...)`
|
|
191
|
+
- Preserves `player` from the outer record
|
|
192
|
+
- Unnests `pairing` array elements as `opponent`
|
|
193
|
+
- For `{pairing: ["Giri", "Gukesh"], player: "Giri"}` this produces:
|
|
194
|
+
- `{player: "Giri", opponent: "Giri"}`
|
|
195
|
+
- `{player: "Giri", opponent: "Gukesh"}`
|
|
196
|
+
|
|
197
|
+
4. **`where player != opponent`** filters out self-matches
|
|
198
|
+
|
|
199
|
+
The result: each pairing expands into both player/opponent perspectives.
|
|
200
|
+
|
|
201
|
+
### Finding Tie-Breaks
|
|
202
|
+
|
|
203
|
+
Now we can append the same count query to find players who faced each other
|
|
204
|
+
more than once:
|
|
205
|
+
|
|
206
|
+
```mdtest-command
|
|
207
|
+
super -s -c "
|
|
208
|
+
split(pairing, ' ')
|
|
209
|
+
| unnest {all_pairings:this, player:this} into (
|
|
210
|
+
unnest {player: this.player, opponent: all_pairings} into (
|
|
211
|
+
where player != opponent
|
|
212
|
+
| cut player, opponent
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
| count(this) by player, opponent
|
|
216
|
+
| where count > 1
|
|
217
|
+
| sort player, opponent
|
|
218
|
+
" pairings.sup
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```mdtest-output
|
|
222
|
+
{player:"Gukesh",opponent:"Wei",count:2}
|
|
223
|
+
{player:"Wei",opponent:"Gukesh",count:2}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## as of versions
|
|
227
|
+
|
|
228
|
+
```mdtest-command
|
|
229
|
+
super --version
|
|
230
|
+
```
|
|
231
|
+
```mdtest-output
|
|
232
|
+
Version: v0.2.0
|
|
233
|
+
```
|