@oxy-hq/cli 0.7.0 → 0.9.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/README.md +131 -8
- package/dist/main.mjs +4909 -3551
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-xxWL3_dz.mjs → mcp-Bdp0rX0l.mjs} +2 -2
- package/dist/{mcp-xxWL3_dz.mjs.map → mcp-Bdp0rX0l.mjs.map} +1 -1
- package/package.json +5 -5
- package/skills/oxy-agents/SKILL.md +318 -0
- package/skills/oxy-agents/references/quick-reference.md +222 -0
- package/skills/oxy-agents/templates/agentic-template.yml +141 -0
- package/skills/oxy-airway-pipelines/SKILL.md +223 -316
- package/skills/oxy-automations/SKILL.md +61 -15
- package/skills/oxy-cli/SKILL.md +37 -4
- package/skills/oxy-custom-apps/SKILL.md +193 -291
- package/skills/oxy-custom-apps/references/minimal-starter.md +63 -0
- package/skills/oxy-custom-apps/references/performance-and-usage.md +105 -0
- package/skills/oxy-custom-apps/references/reading-data.md +106 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +267 -0
- package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
- package/skills/oxy-data-apps/SKILL.md +362 -0
- package/skills/oxy-data-apps/references/examples.md +263 -0
- package/skills/oxy-data-apps/references/quick-reference.md +171 -0
- package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
- package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
- package/skills/oxy-data-architecture/SKILL.md +137 -152
- package/skills/oxy-evals/SKILL.md +362 -0
- package/skills/oxy-evals/references/diagnosis.md +243 -0
- package/skills/oxy-evals/references/results-json.md +145 -0
- package/skills/oxy-evals/references/writing-expected.md +112 -0
- package/skills/oxy-semantic-modeling/SKILL.md +239 -59
- package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
- package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
- package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
- package/template/.github/workflows/publish.yaml +15 -13
- package/template/.nvmrc +1 -1
- package/template/.oxyc-managed +5 -5
- package/template/README.md +21 -10
- package/template/package.json +3 -2
- package/template/pnpm-workspace.yaml +28 -17
|
@@ -1,338 +1,245 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oxy-airway-pipelines
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when writing, reviewing, or debugging an Oxy Airway ELT pipeline (`.airway.yml`) — sources,
|
|
5
|
+
the `kind` values Oxy actually wires, credentials, reset, rewind and retry — or when one
|
|
6
|
+
misbehaves: "unsupported source kind", a pipeline that parses then fails at run time,
|
|
7
|
+
credentials that do not resolve, a *pipeline* run landing `completed_with_errors` (an
|
|
8
|
+
*automation* run with that status is `oxy-automations`), a schema that will not change, missing
|
|
9
|
+
history after a backfill, or duplicate rows. Carries the traps the published pipeline reference
|
|
10
|
+
does not.
|
|
4
11
|
---
|
|
5
12
|
|
|
6
13
|
# Oxy Airway pipelines
|
|
7
14
|
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- **`
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
anyone there for Airway.
|
|
52
|
-
|
|
53
|
-
## Anatomy
|
|
54
|
-
|
|
55
|
-
`AirwayPipelineSpec` (`config.rs`) is `#[serde(deny_unknown_fields)]`, so an
|
|
56
|
-
unrecognised top-level key is a **hard parse error**, not a silent no-op. That
|
|
57
|
-
is the friendly half; the traps below are the rest.
|
|
58
|
-
|
|
59
|
-
| Key | Required | Default | Notes |
|
|
60
|
-
|---|---|---|---|
|
|
61
|
-
| `name` | **yes** | — | validated non-empty |
|
|
62
|
-
| `source` | **yes** | — | `{kind: String, config: <free-form>}` |
|
|
63
|
-
| `destination` | **yes** | — | `{database, dataset_name, schema_separator?}` |
|
|
64
|
-
| `description` | no | — | free text; `>` block scalar is house style |
|
|
65
|
-
| `resources` | no | `[]` = all | subset of the source's advertised resources |
|
|
66
|
-
| `concurrency` | no | **`1`** | validated, capped at `MAX_CONCURRENCY = 16` |
|
|
67
|
-
| `allow_concurrent_runs` | no | `false` | single-flight; see run semantics below |
|
|
68
|
-
| `streaming` | no | `true` | falls back to the bulk path transparently |
|
|
69
|
-
| `channel_capacity` | no | `2 * concurrency` | streaming path only |
|
|
70
|
-
|
|
71
|
-
The corpus's smallest file (`quickbooks_financials.airway.yml`; IDs redacted and
|
|
72
|
-
comments stripped — the real file's comments cover `database:` resolving
|
|
73
|
-
credentials at run time via `airhouse_managed` ephemeral creds, plus commented
|
|
74
|
-
`resources:` / `streaming:` exemplars):
|
|
75
|
-
|
|
76
|
-
```yaml
|
|
77
|
-
name: quickbooks_financials
|
|
78
|
-
source:
|
|
79
|
-
kind: quickbooks
|
|
80
|
-
config:
|
|
81
|
-
client_id: <literal> # not a secret; see credentials below
|
|
82
|
-
client_secret_var: QB_CLIENT_SECRET
|
|
83
|
-
refresh_token_var: QB_REFRESH_TOKEN
|
|
84
|
-
realm_id: "<literal>"
|
|
85
|
-
destination:
|
|
86
|
-
database: pokehouse
|
|
87
|
-
dataset_name: quickbooks_financials
|
|
88
|
-
concurrency: 1
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**All 19 real files use that `destination: {database, dataset_name}` reference
|
|
92
|
-
form**, resolved by the `agentic-pipeline` executor at run time. `config.rs` also
|
|
93
|
-
accepts an `Inline{kind, config}` destination, but its own doc comment calls that
|
|
94
|
-
a test-fixture / post-resolution shape — no real file uses it. Field presence
|
|
95
|
-
across the 19: `name`/`source`/`destination` 19, `concurrency` 14, `description`
|
|
96
|
-
12, `schema_separator` 1, and `resources` / `allow_concurrent_runs` / `streaming`
|
|
97
|
-
/ `channel_capacity` **0** — those four are off the beaten path.
|
|
98
|
-
|
|
99
|
-
**The extension is `.airway.yml`, and it is load-bearing.** All 19 use it; none
|
|
100
|
-
uses a bare `.yml`. The compile-boundary walker
|
|
101
|
-
(`crates/oxy-compile/src/walker.rs:170`) globs literally `**/*.airway.yml` to
|
|
102
|
-
produce `FileKind::AirwayPipeline` — a `pipelines/foo.yml` is not a broken
|
|
103
|
-
pipeline, it is *invisible to compilation*, with no error to explain it.
|
|
104
|
-
|
|
105
|
-
## The wired `kind` set — the trap that costs the most time
|
|
106
|
-
|
|
107
|
-
**Oxy wires a strict subset of the engine's sources.** The upstream airway crate
|
|
108
|
-
registers roughly **40** source modules; `source_factory.rs` gives **14** of them
|
|
109
|
-
a concrete arm. YAML has no `kind` enum — `SourceConfig.kind` is a plain `String`
|
|
110
|
-
— so `kind: shopify` parses perfectly and only then fails at *run time*, in the
|
|
111
|
-
`other =>` fallback arm of the dispatch `match` inside
|
|
112
|
-
`source_factory.rs`'s `build_source_connector_inner`, with
|
|
113
|
-
``unsupported source kind `shopify`. Wire it up in
|
|
114
|
-
agentic_airway::source_factory::build_source_connector — …`` (cited by symbol,
|
|
115
|
-
not line: that arm is the *last* one in the table, so every source added above
|
|
116
|
-
it shifts it. Two did on 2026-08-17 alone — `ubereats` (#2937) and `netsuite`
|
|
117
|
-
(#2939).)
|
|
118
|
-
|
|
119
|
-
The 14 wired kinds, and how hard the real corpus exercises each:
|
|
120
|
-
|
|
121
|
-
| `kind` | Real pipelines using it |
|
|
122
|
-
|---|---|
|
|
123
|
-
| `http_file` | 5 (census LODES + gazetteer pulls) |
|
|
124
|
-
| `quickbooks` | 5 (see below — not one pattern) |
|
|
125
|
-
| `rest_api` | 3 (`census_acs`, `nces_schools`, `yelp_fusion`) |
|
|
126
|
-
| `clickhouse` | 2 (`clickhouse_ingest`, `clickhouse_mirror`) |
|
|
127
|
-
| `besttime` | 1 |
|
|
128
|
-
| `overpass` | 1 (`osm_augmentation`) |
|
|
129
|
-
| `overture` | 1 (`overture_places`) |
|
|
130
|
-
| `toast` | 1 (`restaurant_analytics`) |
|
|
131
|
-
| `filesystem`, `sql_database`, `postgres_cdc`, `weather`, `netsuite`, `ubereats` | 0 — wired, never exercised here |
|
|
132
|
-
|
|
133
|
-
Registered upstream but **not** wired here, so unavailable today: `shopify`,
|
|
15
|
+
**The reference is upstream — read it first:**
|
|
16
|
+
<https://www.oxygen-hq.com/docs/guide/build/data-infrastructure/airway/pipelines>
|
|
17
|
+
(keys, supported `kind` values, credentials, how a run works), with the
|
|
18
|
+
overview beside it at
|
|
19
|
+
<https://www.oxygen-hq.com/docs/guide/build/data-infrastructure/airway>.
|
|
20
|
+
|
|
21
|
+
That page owns the key table and the basic run model. This skill does not
|
|
22
|
+
restate them; it carries the traps that cost time, from one production
|
|
23
|
+
workspace's 19 pipelines.
|
|
24
|
+
|
|
25
|
+
## A pinned engine, so the wired set moves
|
|
26
|
+
|
|
27
|
+
Airway is an external engine. Each Oxy release pins one engine version and
|
|
28
|
+
gives a concrete connector to a **subset** of the sources that engine ships.
|
|
29
|
+
The lists below were checked against the engine Oxy pinned in October 2026
|
|
30
|
+
(airway 0.1.51). A newer Oxy may wire more; when this page and a run disagree,
|
|
31
|
+
the run's `unsupported source kind` error is the authority.
|
|
32
|
+
|
|
33
|
+
## Anatomy traps
|
|
34
|
+
|
|
35
|
+
- **Strict at the top, strict-later inside `config:`.** An unknown top-level
|
|
36
|
+
key is a parse error. The `config:` block is free-form when the file is
|
|
37
|
+
parsed and checked only when a run builds the connector — and most kinds
|
|
38
|
+
then reject an unknown key. So a typo inside `config:` sails through
|
|
39
|
+
compilation and fails the first run with `invalid <kind> config: unknown
|
|
40
|
+
field`.
|
|
41
|
+
- **Use the reference destination form** — `destination: { database,
|
|
42
|
+
dataset_name }`, resolved to a `config.yml` database at run time. An inline
|
|
43
|
+
destination shape also parses, but it exists for tests; no real pipeline uses
|
|
44
|
+
it.
|
|
45
|
+
- **`name` must not start with `preview:`** — the prefix is reserved and
|
|
46
|
+
refused.
|
|
47
|
+
- **Off the beaten path:** across 19 real files, `resources`,
|
|
48
|
+
`allow_concurrent_runs`, `streaming` and `channel_capacity` appear **zero**
|
|
49
|
+
times. They work, but there is no house precedent to copy.
|
|
50
|
+
|
|
51
|
+
## Source kinds: what is wired
|
|
52
|
+
|
|
53
|
+
**Wired (16):** `rest_api`, `http_file`, `filesystem`, `google_sheets`,
|
|
54
|
+
`sql_database`, `postgres_cdc`, `clickhouse`, `toast`, `ubereats`, `sp_api`,
|
|
55
|
+
`quickbooks`, `netsuite`, `weather`, `besttime`, `overture`, `overpass`.
|
|
56
|
+
|
|
57
|
+
**Shipped by the engine but not wired — unavailable in Oxy:** `shopify`,
|
|
134
58
|
`stripe`, `github`, `hubspot`, `salesforce`, `slack`, `jira`, `notion`,
|
|
135
|
-
`zendesk`, `google_ads`, `google_analytics`, `
|
|
136
|
-
`
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
built on
|
|
143
|
-
`rest_api` config
|
|
144
|
-
source with no dedicated
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
59
|
+
`zendesk`, `google_ads`, `google_analytics`, `facebook_ads`, `airtable`,
|
|
60
|
+
`mongodb`, `kafka`, `kinesis`, and more. `kind` is a free-text field, so
|
|
61
|
+
`kind: shopify` parses perfectly and fails only when the pipeline runs.
|
|
62
|
+
**Reading the engine's own source list and concluding a `kind` works here is
|
|
63
|
+
the most expensive mistake this file format offers.**
|
|
64
|
+
|
|
65
|
+
**`rest_api` is the escape hatch.** The engine's vendor connectors (`shopify`,
|
|
66
|
+
`github`, `stripe`, …) are built on its generic REST source, so most can be
|
|
67
|
+
written directly as a `rest_api` config with its own pagination and `auth`
|
|
68
|
+
settings. A source with no dedicated kind is a `rest_api` config, not a blocker.
|
|
69
|
+
|
|
70
|
+
**No Excel.** `filesystem` reads `json`, `jsonl` and `csv` only (from a local
|
|
71
|
+
path or `s3://`, `gs://`, `az://` plus a glob `pattern`); `ubereats` reads its
|
|
72
|
+
`.csv` reports only; `http_file` downloads a file over HTTP. An `.xlsx`
|
|
73
|
+
workbook has **no Airway path** — read as CSV it is binary noise. Export it to
|
|
74
|
+
CSV, or keep the data in a Google Sheet and use `google_sheets`.
|
|
75
|
+
|
|
76
|
+
**Destinations** are `airhouse` (production — where every real pipeline lands)
|
|
77
|
+
and `postgres`; a `memory` destination exists for tests only.
|
|
78
|
+
|
|
79
|
+
### `quickbooks`: two custody modes, never mixed
|
|
80
|
+
|
|
81
|
+
QuickBooks is the one source whose token the host must plumb in, and it comes
|
|
82
|
+
in two mutually exclusive custody modes. A config that mixes them **fails
|
|
83
|
+
closed** rather than guessing:
|
|
84
|
+
|
|
85
|
+
- **Rotating custody** — `client_secret_var` + `refresh_token_var`: the
|
|
86
|
+
pipeline refreshes against Intuit itself and stores the new refresh token.
|
|
161
87
|
- **Read-only custody** — `access_token_var` alone: some *other* component owns
|
|
162
88
|
rotation, and the value is re-read per request rather than substituted once,
|
|
163
|
-
since a 60-minute token pinned at the start of a long backfill expires
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
89
|
+
since a 60-minute token pinned at the start of a long backfill expires
|
|
90
|
+
mid-run.
|
|
91
|
+
|
|
92
|
+
**Why that matters far beyond this file:** Intuit voids the old refresh token
|
|
93
|
+
whenever it issues a new one, so two components rotating the same grant
|
|
94
|
+
deadlock into `invalid_grant` — each invalidating the other's — until a human
|
|
95
|
+
re-authorises by hand. Exactly one component may ever hold rotating custody;
|
|
96
|
+
`access_token_var` is how you keep a pipeline out of that role.
|
|
97
|
+
|
|
98
|
+
Hence QuickBooks pipelines in one workspace are often **not one pattern**: a
|
|
99
|
+
base pipeline that rotates, and per-location pipelines that are read-only
|
|
100
|
+
because a scheduled job owns rotation. Copying "the QuickBooks pipeline" as a
|
|
101
|
+
template means first knowing which custody mode that company's secret was
|
|
102
|
+
provisioned under.
|
|
103
|
+
|
|
104
|
+
### `google_sheets`: store the key, never a token
|
|
105
|
+
|
|
106
|
+
- Set **`service_account_json_var`** to a secret holding the service-account
|
|
107
|
+
**JSON key**. A fresh, read-only access token is minted from it on every run.
|
|
108
|
+
Do not store an access token: it lives an hour, so the pipeline would succeed
|
|
109
|
+
once and then return 401 forever. With no key resolved, the run fails with
|
|
110
|
+
`no access token was resolved` rather than a misleading "not found".
|
|
111
|
+
- **`ranges:` decides the resource names.** Each entry (`Main`, `Main!A:S`)
|
|
112
|
+
becomes one resource named after the part before `!`, lowercased, spaces
|
|
113
|
+
turned into underscores — `Main!A:S` is the resource `main`, and that is the
|
|
114
|
+
name `resources:` has to list. Empty `ranges` loads every sheet as one
|
|
115
|
+
resource called `sheet_data`.
|
|
116
|
+
|
|
117
|
+
### `sp_api` (Amazon): two settings with no safe default
|
|
118
|
+
|
|
119
|
+
- **`default_start` is the entire backfill policy.** The connector only pulls
|
|
120
|
+
*forward*, from one window spanning `default_start` to now, with no
|
|
121
|
+
chunking. Too recent loses history *quietly* — nothing signals it, and only a
|
|
122
|
+
cursor rewind brings it back. Too far back is worse: a window too large for
|
|
123
|
+
Amazon to build in time fails identically on every subsequent run, so the
|
|
124
|
+
pipeline stalls permanently instead of catching up. Pick it deliberately.
|
|
125
|
+
- **`partner_type` (`seller`, the default, or `vendor`) belongs to the
|
|
126
|
+
credential.** It decides which reports exist at all; the wrong value pulls
|
|
127
|
+
nothing and refuses every named resource. A business with both accounts
|
|
128
|
+
needs two pipelines.
|
|
129
|
+
|
|
130
|
+
## Credentials: per-kind `*_var` keys
|
|
131
|
+
|
|
132
|
+
A `*_var` key names a secret **key**, never a value; the run resolves it from
|
|
133
|
+
the secret manager, writes the value under the plain name (`client_secret_var`
|
|
134
|
+
→ `client_secret`), and removes the `_var` key. **The sets are per kind and do
|
|
135
|
+
not overlap — read the row for your kind, never borrow from another:**
|
|
136
|
+
|
|
137
|
+
| `kind` | `*_var` keys it manages |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `toast` | `client_id_var`, `client_secret_var` |
|
|
140
|
+
| `quickbooks` | `client_secret_var`, `refresh_token_var`, `client_id_var` — or `access_token_var` alone (read-only custody) |
|
|
141
|
+
| `sp_api` | `client_secret_var`, `refresh_token_var` |
|
|
142
|
+
| `netsuite` | `private_key_var` — a multi-line PEM; the secret store must keep its newlines |
|
|
143
|
+
| `clickhouse` | `password_var` |
|
|
144
|
+
| `sql_database`, `postgres_cdc` | `connection_string_var` (a whole DSN) |
|
|
145
|
+
| `weather`, `besttime` | `api_key_var` |
|
|
146
|
+
| `google_sheets` | `service_account_json_var` |
|
|
147
|
+
| `rest_api` | nested under `config.auth`: `token_var` (bearer) or `key_var` (`api_key` header or query) |
|
|
148
|
+
|
|
149
|
+
Any other kind manages no credentials.
|
|
150
|
+
|
|
151
|
+
- **Borrowing across rows fails at run time, not at parse time.** Put
|
|
152
|
+
`refresh_token_var` on a `toast` config, or `client_id_var` on an `sp_api`
|
|
153
|
+
config, and nothing resolves or removes it; it reaches the connector's strict
|
|
154
|
+
config and the run dies on `unknown field` — in a file that compiled cleanly.
|
|
155
|
+
- **The `_var` form is valid only in the YAML.** The connector never accepts
|
|
156
|
+
`password_var` and the like; it only ever sees the resolved plain field.
|
|
157
|
+
- **An unresolvable secret name fails the run and names the secret. A secret
|
|
158
|
+
that resolves to an empty string counts as unset** — the field is left out
|
|
159
|
+
entirely (ClickHouse then sends no password at all, not an empty one).
|
|
222
160
|
|
|
223
161
|
## Run semantics
|
|
224
162
|
|
|
225
163
|
- **Partial failures are their own status.** A run that loaded some resources
|
|
226
|
-
and failed others records as **`completed_with_errors
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
it
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
and a
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
one is `airway_run_extensions.resume_state` (beside `retry_count`), used only
|
|
251
|
-
by **backfill** runs; retry there is *reset-in-place*, re-driving the same
|
|
252
|
-
`run_id` rather than cloning a new run (Toast:
|
|
253
|
-
`with_resumable_backfill_window`).
|
|
164
|
+
and failed others records as **`completed_with_errors`**. A dashboard or
|
|
165
|
+
alert that only matches "failed" reports it as a clean success.
|
|
166
|
+
- **Schema migration is additive-only, structurally.** A run can add a table, a
|
|
167
|
+
column, or a *variant* column for a type conflict (the old column is left
|
|
168
|
+
alone); it can never drop or narrow a column in place. **A pipeline sitting
|
|
169
|
+
on a wrong schema never self-heals, however many times you rerun it.**
|
|
170
|
+
- **Two recoveries, and they are not interchangeable:**
|
|
171
|
+
- **Reset schema** drops the pipeline's destination tables and clears its
|
|
172
|
+
cursors, so the next run re-infers a fresh schema and re-pulls everything.
|
|
173
|
+
Destructive on purpose.
|
|
174
|
+
- **Rewind cursors** (all of them, or one resource's) keeps every table and
|
|
175
|
+
row and only moves the cursor back, so the next run re-pulls a window. It
|
|
176
|
+
is **refused** where re-pulling would duplicate rows — an append-only
|
|
177
|
+
table, or a merge without a key — unless explicitly forced. A
|
|
178
|
+
`replace` table, or a keyed merge, converges safely.
|
|
179
|
+
- **A retry resumes from the cursor; it does not re-pull history.** Cursors
|
|
180
|
+
are kept per workspace and pipeline, and advance only after a load commits,
|
|
181
|
+
so ordinary retries are gap-free. A backfill run keeps its own resumable
|
|
182
|
+
window and is retried in place rather than cloned.
|
|
183
|
+
- **A `modified`/`updated_at` cursor can lie.** Some sources change a record
|
|
184
|
+
without advancing its modified timestamp (Toast does not advance it when a
|
|
185
|
+
card payment is captured), so an incremental run never sees the change. A
|
|
186
|
+
backfill meant to repair that must widen **both** ends of its window — not
|
|
187
|
+
only the start — or it re-reads the same stale rows.
|
|
254
188
|
|
|
255
189
|
### Duplicate rows in a `replacing` table
|
|
256
190
|
|
|
257
191
|
`write_disposition: replacing` never writes the served table directly — it
|
|
258
|
-
appends to a buffer
|
|
259
|
-
|
|
260
|
-
`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
it
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
resolves the race rather than application code (`extension/pipeline_lease.rs:1`,
|
|
285
|
-
`:16`). A process-local mutex was rejected explicitly: Oxy runs multiple
|
|
286
|
-
`oxy-serve`/`oxy-worker` replicas, and the engine's own `COMPACTION_GATE` rests
|
|
287
|
-
on a single-writer assumption that stopped holding once the data plane scaled
|
|
288
|
-
past one replica (`pipeline_lease.rs:7`). Two consequences follow.
|
|
289
|
-
**`allow_concurrent_runs: true` turns the guard off** — `config.rs:44` says to
|
|
290
|
-
set it only on a pipeline owning no cursor and writing no `replacing` table,
|
|
291
|
-
since on a `replacing` pipeline it re-admits exactly this bug. And **a
|
|
292
|
-
dead-lettered run never releases its lease**: the 6-hour `LEASE_TTL_SECS` is a
|
|
293
|
-
crash backstop, not a reclaim policy, and the generic reaper cannot touch the
|
|
294
|
-
table without leaking a domain table into `agentic-runtime`
|
|
295
|
-
(`pipeline_lease.rs:73`, `:104`) — so a pipeline stuck "busy" with nothing
|
|
296
|
-
running is that gap, and wants the manual release, not a six-hour wait.
|
|
192
|
+
appends to a buffer in a sibling *schema*, `<schema>_raw.<table>`, that a
|
|
193
|
+
merge-on-read fold rebuilds latest-wins (chosen to avoid a full-table
|
|
194
|
+
`MERGE INTO` that runs the data plane out of memory). **That fold runs twice
|
|
195
|
+
over: inline at the end of every successful load, and again on the scheduled
|
|
196
|
+
warehouse vacuum, from identical SQL.** Either can be the one that promotes a
|
|
197
|
+
given row.
|
|
198
|
+
|
|
199
|
+
That shape is where duplicates come from: **two folds whose snapshots overlap
|
|
200
|
+
each purge against a base the other has not committed yet, so both versions of
|
|
201
|
+
a changed row survive.** Measured, not hypothetical — dozens of excess rows in
|
|
202
|
+
a production orders table, **every duplicate pair spanning two
|
|
203
|
+
`_aw_load_id`s**. That span is the diagnostic, so check it *before* auditing
|
|
204
|
+
your own config: a pair straddling two load ids is this, not your config — and
|
|
205
|
+
if it does not straddle, this section does not explain your duplicates.
|
|
206
|
+
|
|
207
|
+
The guard is a **single-flight lease**: at most one active run per
|
|
208
|
+
(workspace, pipeline), taken atomically in the database so that replicas
|
|
209
|
+
racing each other cannot both win. Two consequences:
|
|
210
|
+
|
|
211
|
+
- **`allow_concurrent_runs: true` turns the guard off.** Set it only on a
|
|
212
|
+
pipeline that owns no cursor and writes no `replacing` table; on a
|
|
213
|
+
`replacing` pipeline it re-admits exactly this bug.
|
|
214
|
+
- **A dead-lettered run never releases its lease.** The lease's 6-hour expiry
|
|
215
|
+
is a crash backstop, not a reclaim policy, so a pipeline stuck "busy" with
|
|
216
|
+
nothing running is this gap — ask an operator to release the lease rather
|
|
217
|
+
than waiting six hours.
|
|
297
218
|
|
|
298
219
|
The lease stops *new* overlapping folds; being an admission guard, it cannot
|
|
299
|
-
repair rows that already landed. Those need Reset schema
|
|
220
|
+
repair rows that already landed. Those need **Reset schema**.
|
|
300
221
|
|
|
301
222
|
## Traps
|
|
302
223
|
|
|
303
|
-
**
|
|
304
|
-
silent.**
|
|
305
|
-
`
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
`
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
the
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
both keys together, and these are hand-edited files. State `secure:` explicitly
|
|
326
|
-
and match it to the port, as `clickhouse_mirror.airway.yml` does (`8123`/`false`).
|
|
327
|
-
(b) Socket timeouts belong in `settings:`, never a SQL `SETTINGS` clause:
|
|
328
|
-
ClickHouse fixes the response socket's timeout at request setup, *before* parsing
|
|
329
|
-
the query body, so an inline `SETTINGS http_send_timeout=…` is accepted and
|
|
330
|
-
ignored — exactly what someone reaches for when tuning a slow mirror.
|
|
331
|
-
|
|
332
|
-
**A `kind` claim is only as good as the pin it was checked against, and in-tree
|
|
333
|
-
comments do not self-update.** `overture_places.airway.yml` still carries a
|
|
334
|
-
header saying the connector has not landed and the airway rev must be pinned to
|
|
335
|
-
a build registering `kind: overture` — which at 0.1.30 it does, so the comment
|
|
336
|
-
has been stale for a while with nothing to flag it. Read that as the standing
|
|
337
|
-
warning about this whole document: verify against `source_factory.rs` at the rev
|
|
338
|
-
you are running.
|
|
224
|
+
**A `# yaml-language-server: $schema=` header on a pipeline is dead, and its
|
|
225
|
+
failure is silent.** Some real pipelines open with one pointing at a
|
|
226
|
+
`json-schemas/pipeline.json` on GitHub. That URL returns **404**, and no JSON
|
|
227
|
+
schema for `.airway.yml` is published anywhere. Start from such a file and you
|
|
228
|
+
inherit an editor integration that validates nothing, warns about nothing, and
|
|
229
|
+
looks like it works. Delete the line.
|
|
230
|
+
|
|
231
|
+
**Two `clickhouse` source footguns, both silent.** (a) `port: 8443` with no
|
|
232
|
+
`secure:` silently uses plaintext on a TLS port — tolerable only because the
|
|
233
|
+
setup wizard emits both keys together, and these are hand-edited files. State
|
|
234
|
+
`secure:` explicitly and match it to the port (`8123`/`false`,
|
|
235
|
+
`8443`/`true`). (b) Socket timeouts belong in the source's `settings:` map,
|
|
236
|
+
never in a SQL `SETTINGS` clause: ClickHouse fixes the response socket's
|
|
237
|
+
timeout at request setup, *before* parsing the query body, so an inline
|
|
238
|
+
`SETTINGS http_send_timeout=…` is accepted and ignored — exactly what someone
|
|
239
|
+
reaches for when tuning a slow mirror.
|
|
240
|
+
|
|
241
|
+
**Comments inside pipeline files do not self-update.** One real pipeline still
|
|
242
|
+
carries a header saying its connector "has not landed" and the engine must be
|
|
243
|
+
pinned to a build registering that `kind` — long after it was wired, with
|
|
244
|
+
nothing to flag the comment as stale. Treat every in-file claim about what is
|
|
245
|
+
supported, this page included, as dated; the run is the authority.
|