toga-ai 1.0.308 → 1.0.309

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.
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-07
9
+ updated: 2026-07-09
10
10
  owners: [jcardinal, mhammontree, bala]
11
11
  files:
12
12
  - Core/
@@ -168,35 +168,62 @@ its own header.)
168
168
  5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
169
169
  work in a `HISTORIC` folder.
170
170
 
171
- ## Bulk data loads — one statement, not many
171
+ ## Bulk data loads — batch, and stage large sets in a temp table
172
172
 
173
- When a change inserts many rows (reference-data / code-table loads), write it as a **single
174
- bulk `INSERT`**, not one `INSERT` statement per row. The database is billed per query and the
175
- external executor runs each statement as its own round-trip, so N separate inserts cost N times
176
- the round-trips (and query cost) of one batched statement. This is a standing team preference.
173
+ When a change inserts many rows (reference-data / code-table loads), write it as **batched
174
+ statements**, not one `INSERT` per row. The database is billed per query and the external
175
+ executor runs each statement as its own round-trip, so N separate inserts cost N times the
176
+ round-trips (and query cost) of a batched load.
177
177
 
178
- - Use one `INSERT ... VALUES (...),(...),...` or `INSERT ... SELECT` over a derived
179
- (`UNION ALL`) row set.
180
- - To stay **idempotent** without a unique key to `INSERT IGNORE` against, anti-join the source
181
- set to the target and insert only the misses:
178
+ To stay **idempotent** without a unique key to `INSERT IGNORE` against, anti-join the source
179
+ set to the target and insert only the misses.
180
+
181
+ **Small sets (a few hundred rows):** a single `INSERT ... VALUES (...),(...),...`, or an
182
+ `INSERT ... SELECT` over a `UNION ALL` derived row set, is fine:
182
183
 
183
184
  ```sql
184
185
  INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
185
- SELECT
186
- src.uuid,
187
- src.keyColumn,
188
- src.otherColumn
186
+ SELECT src.uuid, src.keyColumn, src.otherColumn
189
187
  FROM (
190
188
  SELECT '<uuid>' AS uuid, '<key>' AS keyColumn, '<value>' AS otherColumn
191
189
  UNION ALL SELECT ...
192
190
  ) AS src
193
191
  LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
194
- WHERE
195
- existing.id IS NULL;
192
+ WHERE existing.id IS NULL;
193
+ ```
194
+
195
+ **⚠ Large sets — never build them as a long `UNION ALL` chain.** MySQL parses a `UNION ALL`
196
+ chain **recursively**, one parser-stack frame per link. Around ~2,000+ links overflow the
197
+ default 256 KB per-thread stack and the statement dies at **parse time** (before a single row
198
+ is processed) with:
199
+
200
+ ERROR 1436 (HY000): Thread stack overrun: <n> bytes used of a 262144 byte stack, and
201
+ 20000 bytes needed. Use 'mysqld --thread_stack=#' to specify a bigger stack.
202
+
203
+ This is a parser-recursion limit — **not** a row-volume, `max_allowed_packet`, or
204
+ server-misconfiguration issue, and **do not** raise `thread_stack` on the server for a one-off
205
+ load. Instead, stage the rows in a **session-scoped temp table via a flat multi-row `VALUES`
206
+ list** (which the parser reads as a flat list, no recursion), then run the anti-join once from
207
+ the staging table:
208
+
209
+ ```sql
210
+ CREATE TEMPORARY TABLE _stage (
211
+ uuid VARCHAR(36) NOT NULL, keyColumn VARCHAR(255) NOT NULL, otherColumn TEXT NULL);
212
+ INSERT INTO _stage (uuid, keyColumn, otherColumn)
213
+ VALUES ('<uuid>','<key>','<value>'), ('<uuid>','<key>','<value>'), ... ; -- flat, thousands of rows
214
+ INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
215
+ SELECT src.uuid, src.keyColumn, src.otherColumn
216
+ FROM _stage AS src
217
+ LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
218
+ WHERE existing.id IS NULL;
219
+ DROP TEMPORARY TABLE _stage;
196
220
  ```
197
221
 
198
- - Keep the single statement well under `max_allowed_packet` (64 MB default) — a few thousand
199
- rows is comfortably fine.
222
+ The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect even if
223
+ the run aborts). A multi-row `VALUES` load of a few thousand rows is a few hundred KB — well
224
+ under the 64 MB `max_allowed_packet` default. First hit in
225
+ `Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql` (2,200 AIG SaleItem codes); see
226
+ `clients/aig/features/entitlement-intake.md` → *Uploading new codes*.
200
227
 
201
228
  ## Self-referencing DELETE — wrap the subquery in a derived table
202
229
 
@@ -88,17 +88,27 @@ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Des
88
88
  3. Generate **fully-random UUIDs** (e.g. `uuid4`), never the time-based MySQL `UUID()` —
89
89
  per the 2.0 standard. Bake literal UUIDs into the SQL since a migration can't call PHP's
90
90
  `_String::generateUuid()`.
91
- 4. Use a **single bulk `INSERT`** (see the dbchanges2 architecture rule), made idempotent with
92
- an anti-join because `Items` has **no unique key on `partNumber`** (so `INSERT IGNORE`
93
- can't help):
91
+ 4. Feed the rows through a **session-scoped temp table populated by a flat multi-row
92
+ `VALUES` list**, then anti-join once into `Items` — idempotent because `Items` has **no
93
+ unique key on `partNumber`** (so `INSERT IGNORE` can't help). Do **not** assemble the rows
94
+ as a long `UNION ALL` derived table: at AIG's ~2,200-code scale that overflows MySQL's
95
+ parser stack and fails at **parse time** with **ERROR 1436 (HY000) Thread stack overrun**
96
+ (see the gotcha below and the dbchanges2 architecture bulk-load rule). A flat `VALUES`
97
+ list does not recurse and stays well under `max_allowed_packet`:
94
98
  ```sql
99
+ CREATE TEMPORARY TABLE _aig_saleitem_stage (
100
+ uuid VARCHAR(36) NOT NULL, partNumber VARCHAR(255) NOT NULL, description TEXT NULL);
101
+ INSERT INTO _aig_saleitem_stage (uuid, partNumber, description)
102
+ VALUES ('<uuid>','<part>','<desc>'), ('<uuid>','<part>','<desc>'), ... ; -- flat, ~2,200 rows
95
103
  INSERT INTO Items (uuid, partNumber, description)
96
104
  SELECT src.uuid, src.partNumber, src.description
97
- FROM ( SELECT '<uuid>' AS uuid, '<part>' AS partNumber, '<desc>' AS description
98
- UNION ALL SELECT ... ) AS src
105
+ FROM _aig_saleitem_stage AS src
99
106
  LEFT JOIN Items existing ON existing.partNumber = src.partNumber
100
107
  WHERE existing.id IS NULL;
108
+ DROP TEMPORARY TABLE _aig_saleitem_stage;
101
109
  ```
110
+ The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect
111
+ even if the run aborts). Do **not** raise `thread_stack` on the server for a one-off load.
102
112
 
103
113
  ## Client variations
104
114
 
@@ -121,6 +131,13 @@ this interceptor or use this dual-purpose Items pattern.
121
131
  codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
122
132
  - **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
123
133
  `INSERT IGNORE`.
134
+ - **A long `UNION ALL` derived table fails at ~2,200 rows with ERROR 1436 "Thread stack
135
+ overrun."** The AIG SaleItem load is exactly this scale. MySQL builds the `UNION ALL`
136
+ chain recursively, so ~2,200 links overflow the default 256 KB per-thread parser stack and
137
+ the statement dies at **parse time** (before any row is processed) — it is a parser-recursion
138
+ limit, **not** a row-volume, `max_allowed_packet`, or server-config problem. Stage the rows
139
+ in a temp table via a flat multi-row `VALUES` list and anti-join from there (see *Uploading
140
+ new codes* step 4). Do not raise `thread_stack` for a one-off load.
124
141
  - **An `entitlementFulfillmentType` EV-12 VALIDATION error is NOT a bad client payload —
125
142
  it's this interceptor's injected field failing to resolve.** AIG never sends that field;
126
143
  the interceptor injects it as `{name: "Repair"}`. The error means the V2 resolver could not
@@ -136,6 +153,11 @@ this interceptor or use this dual-purpose Items pattern.
136
153
 
137
154
  ## Change history
138
155
 
156
+ - 2026-07-09 — TRUE-79534 SaleItem load reworked: the 2,200-code `INSERT ... SELECT FROM
157
+ (<2,199 `UNION ALL` links>)` derived table failed at parse time with ERROR 1436 (Thread
158
+ stack overrun). Rebuilt as a `TEMPORARY` staging table filled by a flat multi-row `VALUES`
159
+ list + the same anti-join into `Items` (identical rows/UUIDs, 0 duplicate partNumbers).
160
+ (mhammontree)
139
161
  - 2026-07-09 — TRUE-79978: root-caused an `entitlementFulfillmentType` EV-12 on beta/QA. The
140
162
  field is injected (name-only) by `prePost`; the beta `Client_Aig.Apis_RecordFields` override
141
163
  row (apiId=2, recordFieldId=1202) had `overrideIsIdentifier=NULL` + `overrideChildPolicy=MATCH_CREATE`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.308",
3
+ "version": "1.0.309",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",