toga-ai 1.0.498 → 1.0.499
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/knowledge/1.0/apps/library/INDEX.md +1 -1
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +79 -5
- package/knowledge/1.0/apps/test/INDEX.md +1 -0
- package/knowledge/1.0/apps/test/features/static-no-db-regression-harness.md +100 -0
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +23 -3
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/architecture.md +46 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md +113 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/aig/features/entitlement-intake.md +40 -15
- package/package.json +1 -1
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
15
15
|
| [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | `App_SystemMonitor_NetSuiteIntegration` (`library/app/systemmonitor/netsuiteintegration.php`, title **"NetSuite Sync Alert"**) is a 1.0 system monitor that watc | library/app/systemmonitor/netsuiteintegration.php, worker/crons/infrastructure/system_monitors.php |
|
|
16
16
|
| [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php, library/app/api/startechticket.php, worker/crons/toga2/startech/common_import_supporting_records.php |
|
|
17
|
-
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php |
|
|
17
|
+
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php, test/@Mark/AIG/test_multi_email.php |
|
|
@@ -12,9 +12,12 @@ files:
|
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- worker/crons/toga2/aig/sync_togasupply_aig.php
|
|
14
14
|
- worker/crons/toga2/wje/sync_togasupply_wje.php
|
|
15
|
+
- test/@Mark/AIG/test_multi_email.php
|
|
15
16
|
related:
|
|
16
17
|
- ../../../2.0/apps/api2/features/nested-relationship-writes.md
|
|
17
18
|
- ../../../clients/aig/features/entitlement-intake.md
|
|
19
|
+
- ../../test/features/static-no-db-regression-harness.md
|
|
20
|
+
- ../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md
|
|
18
21
|
- netsuite-suiteql-api-reference.md
|
|
19
22
|
- netsuite-suiteql-rest-shim.md
|
|
20
23
|
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
@@ -138,7 +141,28 @@ Correct inbound shape:
|
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
The primary **may** be repeated inside the collection — `syncContactToToga1Customer` de-dupes with
|
|
141
|
-
`array_unique
|
|
144
|
+
`array_unique` — and AIG **should** repeat it: if the collection carries extras but **omits** the
|
|
145
|
+
primary, the primary is **not** re-added (the fallback fires only on a *fully empty* list), so the
|
|
146
|
+
1.0 field ends up holding the secondary/tertiary **without** the main address.
|
|
147
|
+
|
|
148
|
+
**A wrong shape degrades silently to primary-only — it does NOT blank the field (corrected
|
|
149
|
+
2026-08-03, proven by the static harness).** With an array of bare strings, `array_column()` finds
|
|
150
|
+
no `emailAddress` key → empty list → **the primary-email fallback fires** → the stored value is
|
|
151
|
+
exactly the primary address, **byte-identical to correct single-email behavior.** So there is **no
|
|
152
|
+
observable difference between "AIG sent the wrong shape" and "AIG hasn't enabled the feature yet"**
|
|
153
|
+
— strictly worse for diagnosis than a blanked field. The **only** reliable verification signal is
|
|
154
|
+
the child row count in `Client_Aig.ContactEmailAddresses`: not the HTTP status, and **not** the 1.0
|
|
155
|
+
`Customers.emailAddress` value.
|
|
156
|
+
|
|
157
|
+
**Storage is bounded — "send as many as you like" is FALSE at the 1.0 layer.** With `'; '` joining
|
|
158
|
+
and ~47-char business addresses, measured lengths against the `varchar(255)` column: 2 addr = 96,
|
|
159
|
+
3 = 145, 5 = 243 chars (all fit); **8 = 390 and 10 = 489 chars → OVERFLOW.** So the practical cap
|
|
160
|
+
is roughly **6–7 business-length addresses.** The committed scope (secondary + tertiary ⇒ 3
|
|
161
|
+
addresses ≈ 145 chars) is comfortably safe, but the 2.0 nested collection is structurally
|
|
162
|
+
**unbounded** while the 1.0 column is not — the "1, 2, 3 or 10 all work with no change on our side"
|
|
163
|
+
pitch is true of 2.0 storage only, and must be qualified for 1.0. Overflow behavior is itself
|
|
164
|
+
machine-dependent (silent truncation under an empty `sql_mode`, `ERROR 1406` under strict mode) —
|
|
165
|
+
see [local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
142
166
|
|
|
143
167
|
**Three distinct layers — only the last one is delimited.** Conflating them is the easy mistake:
|
|
144
168
|
|
|
@@ -164,8 +188,18 @@ tickets) was **explicitly rejected** — see the gotcha about the single-recipie
|
|
|
164
188
|
|
|
165
189
|
### Open items (TRUE-79401)
|
|
166
190
|
|
|
167
|
-
1.
|
|
168
|
-
|
|
191
|
+
1. **Merged: YES. Deployed: still unverified.** Verified 2026-08-03: branch `TRUE-79401`
|
|
192
|
+
(`5aca31ea`) is **fully merged up with `_production`** (`_production` is an ancestor of HEAD,
|
|
193
|
+
working tree clean, local `_production` == `origin/_production` as of the last fetch — no network
|
|
194
|
+
fetch was run). The ticket's own patch is **still +17 / −2 in `app/api/toga2.php`** against
|
|
195
|
+
`_production`: the two merge commits (`90b4178b`, `5aca31ea`) pulled in ~404 insertions of
|
|
196
|
+
**other** tickets' work (TRUE-80408/80374, plus the error-capture and ServiceNow-OAuth commits)
|
|
197
|
+
and **none** of them touched `syncContactToToga1Customer`, the `/contacts` fetch `fields` list, or
|
|
198
|
+
any TRUE-79401 identifier. `php -l` clean on PHP 7.2.33; harness drift guard OK; 12/12 cases pass
|
|
199
|
+
after the merges. **A reviewer should not be alarmed by the file's churn** — the small patch is
|
|
200
|
+
intact under a large merge. **Still open: whether it is deployed to the prod worker.**
|
|
201
|
+
Review technique worth reusing: `git diff _production...HEAD -- <file>` isolates a ticket's real
|
|
202
|
+
contribution from merge noise when an old branch is finally synced.
|
|
169
203
|
2. **Beta verification of the nested write is outstanding.** Assert real
|
|
170
204
|
`Client_Aig.ContactEmailAddresses` **row counts** (never the HTTP status), covering **both** a
|
|
171
205
|
brand-new contact (CREATE) **and** a second entitlement for an **existing** contact (UPDATE).
|
|
@@ -250,7 +284,9 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
250
284
|
framework-level fact captured separately.)
|
|
251
285
|
- **`TOGA_AIG.Customers.emailAddress` is `varchar(255)`** (V1 legacy, latin1_swedish_ci) — wide
|
|
252
286
|
enough to hold several `'; '`-joined emails, so TRUE-79401 needed **no** schema/dbchanges migration
|
|
253
|
-
(the earlier fear that it was ~VARCHAR(55) was wrong)
|
|
287
|
+
(the earlier fear that it was ~VARCHAR(55) was wrong) — but **it is a real ceiling: ~6–7
|
|
288
|
+
business-length addresses** (measured: 3 = 145 chars, 5 = 243 fit; 8 = 390, 10 = 489 overflow).
|
|
289
|
+
The 2.0 collection is unbounded; this column is not. See *Storage is bounded* above. `TOGA_AIG` is the 1.0 legacy DB;
|
|
254
290
|
`Client_Aig` is the 2.0 prod tenant.
|
|
255
291
|
- **The multi-email path is not yet exercised in prod data (TRUE-79401)** — as of 2026-07-28 all 20
|
|
256
292
|
`Client_Aig.ContactEmailAddresses` rows are one-per-contact. The *payload-shape* question that
|
|
@@ -280,10 +316,48 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
280
316
|
was ~VARCHAR(55)" traces to a garbled transcript line — "needs to fifty-five… maybe we should
|
|
281
317
|
change it to a long"; the column is `varchar(255)`. Closed.)
|
|
282
318
|
- **PHP 7.2** target (prod worker/library) — no arrow functions, typed properties, `??=`, `match`.
|
|
283
|
-
Lint with `
|
|
319
|
+
Lint with your **PHP 7.2 CLI** (`php -l <file>`); do not lint 1.0 code with a PHP 8 binary. (The
|
|
320
|
+
per-machine location of each PHP CLI is a developer-local detail and is deliberately not recorded
|
|
321
|
+
here.)
|
|
322
|
+
- **This logic IS testable without a database — and there is a harness.**
|
|
323
|
+
`App_Database::sqlEscape()` needs **no DB connection** (pure `str_replace`), so the email
|
|
324
|
+
transform can be exercised statically. The TRUE-79401 transform is inlined in the DB-writing
|
|
325
|
+
`private static syncContactToToga1Customer()`, so the harness **mirrors** it and guards against
|
|
326
|
+
drift by asserting the production statements verbatim — see
|
|
327
|
+
[static no-DB regression harness](../../test/features/static-no-db-regression-harness.md). **If
|
|
328
|
+
you edit those 5 statements, the harness will refuse to report until it is updated.** The clean
|
|
329
|
+
fix if you are already in there: extract the transform to a `public static` helper so the mirror
|
|
330
|
+
disappears.
|
|
331
|
+
- **Pre-existing (low severity, out of scope): a raw-vs-escaped comparison causes a redundant
|
|
332
|
+
UPDATE every sync.** At the update path (~line 8584) `syncContactToToga1Customer` compares
|
|
333
|
+
`$existingCustomer['emailAddress'] !== $emailAddress` — the DB value from
|
|
334
|
+
`App_Database::fetchRow()` is **unescaped**, while `$emailAddress` has already been through
|
|
335
|
+
`App_Database::sqlEscape()`. For any value containing `'`, `"` or `\` the two can **never** compare
|
|
336
|
+
equal, so that column is rewritten on **every** run. Same pattern applies to `businessName` and
|
|
337
|
+
`phoneNumber` just above. **Predates TRUE-79401**, is a redundant write (not data loss or
|
|
338
|
+
corruption), and was deliberately left out of that ticket's scope — recorded here so a future
|
|
339
|
+
reader knows it is known.
|
|
284
340
|
|
|
285
341
|
## Change history
|
|
286
342
|
|
|
343
|
+
- 2026-08-03 (later pass) — TRUE-79401 **static regression harness + one correction.** BUILT a
|
|
344
|
+
no-DB PHP harness (`test/@Mark/AIG/test_multi_email.php`, 12 cases, PHP 7.2.33, `php -l` clean)
|
|
345
|
+
that feeds mocked `/contacts` fragments through the email transform and asserts the delimited
|
|
346
|
+
string; it **mirrors** the transform (inlined in the DB-writing private method) and **guards
|
|
347
|
+
against drift** by asserting the 5 production statements verbatim, plus parses
|
|
348
|
+
`CUSTOMER_EMAIL_DELIMITER` from source — technique documented in
|
|
349
|
+
[static no-DB regression harness](../../test/features/static-no-db-regression-harness.md).
|
|
350
|
+
**CORRECTION (supersedes the earlier same-day entry):** a bare-string collection does **not**
|
|
351
|
+
blank/vanish the addresses — it **silently degrades to primary-only**, byte-identical to correct
|
|
352
|
+
single-email output, so the wrong shape is indistinguishable from the feature being disabled;
|
|
353
|
+
child row counts in `Client_Aig.ContactEmailAddresses` are the only signal. Also recorded: the
|
|
354
|
+
omitted-primary sub-case (repeat the primary in the array); the **`varchar(255)` ceiling of ~6–7
|
|
355
|
+
business-length addresses** (so "send as many as you like" is false at the 1.0 layer, qualifying
|
|
356
|
+
the earlier pitch); the pre-existing raw-vs-escaped comparison causing a redundant UPDATE every
|
|
357
|
+
sync (~line 8584, out of scope); genericized the lint instruction (was a non-existent absolute
|
|
358
|
+
`C:\xampp7\...` path); and **verified the TRUE-79401 branch is fully merged with `_production`
|
|
359
|
+
with its patch intact at +17/−2 under ~404 insertions of other tickets' merge noise** (deploy to
|
|
360
|
+
the prod worker still unverified). (mhammontree)
|
|
287
361
|
- 2026-08-03 — TRUE-79401 **payload shape RESOLVED** (review/no-code session; sources: AIG's
|
|
288
362
|
field-mapping spreadsheet, the 2026-05-01 Teams meeting, the 2026-05-04 AIG email). Both
|
|
289
363
|
`UserDefined3` and `UserDefined4` map to the **same** nested
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
|
|
10
10
|
| [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
|
|
11
11
|
| [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
|
|
12
|
+
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, library/app/database.php, library/app/api/toga2.php |
|
|
12
13
|
| [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
|
|
13
14
|
| [Talos Knowledge Base Pipeline (Uploader + Processor)](features/talos-kb-pipeline.md) | `team/talos/` holds the two-script web tooling that feeds the **TOGa Talos** (TOGa IQ) AI knowledge bases. | test/team/talos/kb_uploader.php, test/team/talos/kb_processor.php, test/team/talos/kb_processor.ini |
|
|
14
15
|
| [TOGa 2.0 Client Onboarding SQL Generator](features/toga2-client-onboarding-sql.md) | > **Superseded by the browser wizard.** The generation logic here was extracted into the reusable > `OnboardingSqlGenerator` class and wrapped in a local browse | test/team/generate_toga2_onboarding_sql.php |
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: test
|
|
5
|
+
project: Test
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- test/@Mark/AIG/test_multi_email.php
|
|
13
|
+
- library/app/database.php
|
|
14
|
+
- library/app/api/toga2.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../library/features/toga2-api-client-and-bridge.md
|
|
18
|
+
- ../../library/architecture.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL,
|
|
24
|
+
so "just call it" means standing up a client database. This doc records the technique that makes
|
|
25
|
+
a large class of 1.0 logic testable **statically — no DB connection, no framework bootstrap** —
|
|
26
|
+
plus the **source drift guard** that keeps such a test honest when the logic under test cannot
|
|
27
|
+
actually be invoked.
|
|
28
|
+
|
|
29
|
+
First built for TRUE-79401 (`test/@Mark/AIG/test_multi_email.php`): 12 cases feeding mocked 2.0
|
|
30
|
+
`/contacts` payload fragments through the multi-email transform and asserting the delimited
|
|
31
|
+
string that would be written to `TOGA_AIG.Customers.emailAddress`. Runs on PHP 7.2.33, `php -l`
|
|
32
|
+
clean, no database.
|
|
33
|
+
|
|
34
|
+
## Key enabler: `App_Database::sqlEscape()` needs NO database connection
|
|
35
|
+
|
|
36
|
+
This is the fact that unlocks static 1.0 testing and it is documented nowhere else:
|
|
37
|
+
|
|
38
|
+
- `App_Database::sqlEscape()` delegates to `App_Database::mysqlRealEscapeString()`, which is a
|
|
39
|
+
**pure `str_replace`** over `\`, `\x00`, `\n`, `\r`, `'`, `"`, `\x1a`. The
|
|
40
|
+
`mysqli_real_escape_string` implementation is **commented out** in the source.
|
|
41
|
+
- So it needs **no mysqli link and no `App_Registry` lookup**.
|
|
42
|
+
- `library/app/database.php` declares `abstract class App_Database` with **no `extends`** and no
|
|
43
|
+
parse-time dependencies, so a test can `require_once` that one file alone.
|
|
44
|
+
|
|
45
|
+
Consequence: **any 1.0 logic whose only framework dependency is escaping is unit-testable
|
|
46
|
+
statically.** Consequence for correctness testing too — because escaping is a pure string
|
|
47
|
+
transform, a test can assert the *exact* escaped output.
|
|
48
|
+
|
|
49
|
+
## How the harness is structured
|
|
50
|
+
|
|
51
|
+
1. `require_once` `library/app/database.php` only (no bootstrap, no config, no DB).
|
|
52
|
+
2. Build mocked payload fragments as plain `stdClass`/arrays shaped like the 2.0 API response.
|
|
53
|
+
3. Run the transform (see drift guard below) and assert the resulting string.
|
|
54
|
+
4. Cases cover: single address, multiple, duplicates, blank/whitespace entries, empty
|
|
55
|
+
collection (primary fallback), and wrong payload shapes.
|
|
56
|
+
5. Parse `CUSTOMER_EMAIL_DELIMITER` **out of the production source** rather than hardcoding
|
|
57
|
+
`'; '`, so changing the constant does not silently invalidate the expectations.
|
|
58
|
+
|
|
59
|
+
## Drift guard — for logic that cannot be called
|
|
60
|
+
|
|
61
|
+
The TRUE-79401 transform is 3 lines **inlined inside `private static
|
|
62
|
+
App_Api_Toga2::syncContactToToga1Customer()`**, which also writes SQL. It cannot be invoked
|
|
63
|
+
without a live client DB, so the harness must **mirror** it — and a mirror silently rots.
|
|
64
|
+
|
|
65
|
+
The guard: the harness **reads `library/app/api/toga2.php`**, normalizes whitespace, and asserts
|
|
66
|
+
that the **5 production statements it mirrors still appear verbatim**. On any mismatch it prints
|
|
67
|
+
`DRIFT DETECTED` and **refuses to report results at all** (it does not just warn — a passing
|
|
68
|
+
mirror of stale code is worse than no test).
|
|
69
|
+
|
|
70
|
+
This pattern is reusable for **any 1.0 pure-logic fragment buried in a DB-writing private
|
|
71
|
+
method**, which is a large share of `library`.
|
|
72
|
+
|
|
73
|
+
**Cleaner long-term alternative (deliberately NOT done here):** extract the transform into a
|
|
74
|
+
`public static` helper so the test calls production code directly and the mirror disappears
|
|
75
|
+
entirely. That was out of scope because it changes production code; prefer it when you are
|
|
76
|
+
already editing the method.
|
|
77
|
+
|
|
78
|
+
## Gotchas / known issues
|
|
79
|
+
|
|
80
|
+
- **A mirrored test proves the mirror, not production.** The drift guard is what makes it
|
|
81
|
+
trustworthy — never ship a mirror without one.
|
|
82
|
+
- **Static tests cannot validate anything the server decides.** Column truncation, collation
|
|
83
|
+
behavior, strict mode and table-name casing are all server-config dependent — a no-DB pass says
|
|
84
|
+
nothing about them. See
|
|
85
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
86
|
+
- **`@Mark/` (and any `@<name>/`) folders in `test` are per-developer.** `test` is not branched;
|
|
87
|
+
write only in your own folder and ask before touching another developer's.
|
|
88
|
+
- Lint 1.0 code with your **PHP 7.2** CLI (`php -l <file>`) — the prod worker/library target is
|
|
89
|
+
7.2, so 7.4+/8.x syntax must not appear.
|
|
90
|
+
|
|
91
|
+
## Change history
|
|
92
|
+
|
|
93
|
+
- 2026-08-03 — Documented the static no-DB 1.0 harness technique, built for TRUE-79401
|
|
94
|
+
(`test/@Mark/AIG/test_multi_email.php`, 12 cases, PHP 7.2.33). Key enabler recorded:
|
|
95
|
+
`App_Database::sqlEscape()` is a pure `str_replace` (the `mysqli_real_escape_string` path is
|
|
96
|
+
commented out) needing **no DB link or registry**, and `library/app/database.php` can be
|
|
97
|
+
`require_once`d alone. Also recorded the **source drift guard** for logic inlined in a
|
|
98
|
+
DB-writing private method (assert the mirrored production statements appear verbatim; refuse to
|
|
99
|
+
report results on mismatch) and parsing constants out of the source instead of hardcoding them.
|
|
100
|
+
(mhammontree)
|
|
@@ -135,9 +135,20 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
135
135
|
`UserDefined3`) instead of the nested collection. The key is dropped; no child row is created.
|
|
136
136
|
2. **Array of bare strings instead of array of objects.** `"contactEmailAddresses":
|
|
137
137
|
["email2@domain.com"]` instead of `[{"emailAddress": "email2@domain.com"}]`. Nothing is
|
|
138
|
-
created, and any consumer doing `array_column($list, 'emailAddress')` gets an **empty** list
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
created, and any consumer doing `array_column($list, 'emailAddress')` gets an **empty** list.
|
|
139
|
+
|
|
140
|
+
**Corrected 2026-08-03 (proven by harness):** the downstream effect is **not** an empty/blanked
|
|
141
|
+
field. The empty list makes the consumer's **primary-email fallback fire**, so it stores
|
|
142
|
+
**exactly the primary address** — output that is **byte-identical to correct single-email
|
|
143
|
+
behavior**. There is therefore **no observable difference between "the partner sent the wrong
|
|
144
|
+
shape" and "the partner hasn't enabled the extra addresses yet."** That is strictly *worse* for
|
|
145
|
+
diagnosis than a blanked field would be, and it is why the child ROW COUNT is the only reliable
|
|
146
|
+
signal (see below) — the consumer's stored value cannot distinguish the two.
|
|
147
|
+
|
|
148
|
+
Related shape trap in the same family: if the partner sends extras but **omits the primary from
|
|
149
|
+
the collection**, the primary is **not re-added** — the fallback only fires on a *fully empty*
|
|
150
|
+
list — so the consumer stores the secondary/tertiary but **not** the main address. Tell partners
|
|
151
|
+
to **repeat the primary inside the array**.
|
|
141
152
|
|
|
142
153
|
**Operational rules that follow:**
|
|
143
154
|
|
|
@@ -163,6 +174,15 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
163
174
|
|
|
164
175
|
## Change history
|
|
165
176
|
|
|
177
|
+
- 2026-08-03 (later pass) — **Correction, supersedes the entry below on one point.** The bare-string
|
|
178
|
+
collection shape does **not** make the addresses "vanish"/blank the consumer's field: the empty
|
|
179
|
+
`array_column()` result makes the consumer's primary-email fallback fire, so the stored value is
|
|
180
|
+
**byte-identical to correct single-email output** and the wrong shape is **indistinguishable from
|
|
181
|
+
the feature simply not being enabled** — worse for diagnosis, and confirmation that the child row
|
|
182
|
+
count is the only reliable signal. Also added the omitted-primary sub-case (the fallback fires
|
|
183
|
+
only on a *fully empty* list, so a collection lacking the primary stores the extras **without**
|
|
184
|
+
the main address — partners should repeat the primary inside the array). Proven by the TRUE-79401
|
|
185
|
+
static harness. (mhammontree)
|
|
166
186
|
- 2026-08-03 — Documented that V2 **silently ignores unrecognized payload keys and does not
|
|
167
187
|
validate nested-collection element shape**: both a flat/unknown key and an array of bare strings
|
|
168
188
|
(instead of an array of objects) return **HTTP 201** with the data dropped and no error logged
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql |
|
|
7
7
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
8
|
+
| [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
|
|
8
9
|
| [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: [jcardinal, mhammontree, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -417,6 +417,42 @@ under the 64 MB `max_allowed_packet` default. First hit in
|
|
|
417
417
|
`Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql` (2,200 AIG SaleItem codes); see
|
|
418
418
|
`clients/aig/features/entitlement-intake.md` → *Uploading new codes*.
|
|
419
419
|
|
|
420
|
+
### Staging tables MUST pin the collation of the column they join against
|
|
421
|
+
|
|
422
|
+
A `CREATE TEMPORARY TABLE` that omits an explicit `COLLATE` inherits the **connection/database
|
|
423
|
+
default**, which is **not** guaranteed to match the target column. The moment the staging column
|
|
424
|
+
is joined to a real column with a different collation, MySQL raises:
|
|
425
|
+
|
|
426
|
+
ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and
|
|
427
|
+
(utf8mb4_unicode_ci,IMPLICIT) for operation '='
|
|
428
|
+
|
|
429
|
+
The connection default is commonly `utf8mb4_general_ci`, while 2.0 tenant tables are **typically**
|
|
430
|
+
`utf8mb4_unicode_ci` (verified for `Client_Aig.Items.partNumber`) — so the mismatch is the default
|
|
431
|
+
outcome, not the exception. **Always declare the collation explicitly on every staging column that
|
|
432
|
+
participates in a join**, copying it from the column being matched:
|
|
433
|
+
|
|
434
|
+
```sql
|
|
435
|
+
CREATE TEMPORARY TABLE _stage (
|
|
436
|
+
uuid VARCHAR(36) NOT NULL,
|
|
437
|
+
keyColumn VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL, -- pinned to TargetTable.keyColumn
|
|
438
|
+
otherColumn TEXT NULL
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**This is a per-machine failure, not a per-query one.** Whether it fires depends on the server's
|
|
443
|
+
own defaults, so the *same file* passes on one developer's machine and dies on another's — and on
|
|
444
|
+
the production server. Measured: it ran clean on MySQL **8.0.39** and failed with 1267 on
|
|
445
|
+
**8.0.44** with no change to the SQL. The same class of per-machine divergence applies to
|
|
446
|
+
**`sql_mode`** (e.g. `ONLY_FULL_GROUP_BY`/strict-mode rejecting a statement that passed elsewhere),
|
|
447
|
+
**`lower_case_table_names`** (table-name casing resolving locally and 404-ing on Linux), and
|
|
448
|
+
**`thread_stack`** (the `UNION ALL` parse limit above triggering at different sizes).
|
|
449
|
+
|
|
450
|
+
**Rule: a local pass is NOT evidence that a migration is portable.** Anything that depends on a
|
|
451
|
+
server variable must be pinned **in the SQL file itself** — explicit `COLLATE`, explicit column
|
|
452
|
+
casing, no reliance on a permissive `sql_mode`. Verify server-setting parity before trusting a
|
|
453
|
+
local run; see the environment-parity workflow in
|
|
454
|
+
`2.0/apps/_underscore/architecture.md` → *Database architecture*.
|
|
455
|
+
|
|
420
456
|
## Self-referencing DELETE — wrap the subquery in a derived table
|
|
421
457
|
|
|
422
458
|
MySQL forbids a `DELETE` (or `UPDATE`) whose `WHERE` subquery reads **the same table being
|
|
@@ -511,6 +547,15 @@ defined in `2.0/apps/_underscore/architecture.md`, and its change files create/a
|
|
|
511
547
|
tables that `_Model_*` classes map to.
|
|
512
548
|
|
|
513
549
|
## Change history
|
|
550
|
+
- 2026-08-03 — **Added *Staging tables MUST pin the collation of the column they join against*.**
|
|
551
|
+
A `CREATE TEMPORARY TABLE` without an explicit `COLLATE` inherits the connection default
|
|
552
|
+
(commonly `utf8mb4_general_ci`) and dies with **error 1267 illegal mix of collations** when
|
|
553
|
+
joined to a tenant column that is typically `utf8mb4_unicode_ci` (verified for
|
|
554
|
+
`Client_Aig.Items.partNumber`). Pin the collation on every staging column used in a join.
|
|
555
|
+
Reframed as the general rule that **a local pass is not evidence of portability**: the same file
|
|
556
|
+
ran clean on MySQL 8.0.39 and failed on 8.0.44 unchanged, and the same per-machine divergence
|
|
557
|
+
applies to `sql_mode`, `lower_case_table_names`, and `thread_stack`. Anything depending on a
|
|
558
|
+
server variable must be pinned in the SQL file itself. (mhammontree)
|
|
514
559
|
- 2026-07-31 — **Added *`Core.Records` / `Core.RecordFields` — the only hardcoded `id`s on the
|
|
515
560
|
platform* + rule #9.** These two tables are the **only** ones platform-wide whose `id` is a
|
|
516
561
|
team-maintained constant: the next available value is tracked **in the developer chat**, so a
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Local vs prod MySQL config parity — why “it passed locally” is not evidence"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
related:
|
|
12
|
+
- ../architecture.md
|
|
13
|
+
- ../workflows/nonprod-metadata-drift-repair.md
|
|
14
|
+
- ../../../clients/aig/features/entitlement-intake.md
|
|
15
|
+
- ../../../1.0/apps/test/features/static-no-db-regression-harness.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL
|
|
21
|
+
server-configuration differences**. They are not version differences and not environment
|
|
22
|
+
properties — prod and local are both **MySQL 8.0** — so the same SQL can pass on one developer's
|
|
23
|
+
machine, fail on another's, and fail on deploy, with no code change involved.
|
|
24
|
+
|
|
25
|
+
Measured 2026-08-03: **prod Core = 8.0.39**, **local Core_2 = 8.0.44**. Same engine, same major
|
|
26
|
+
version.
|
|
27
|
+
|
|
28
|
+
**Operating rule: a local pass is NOT evidence that a migration is safe.** Run the parity query
|
|
29
|
+
below before trusting a local result, and pin anything the server would otherwise decide for you.
|
|
30
|
+
|
|
31
|
+
## Parity check — run this on BOTH the target environment and locally
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
SELECT VERSION(), @@collation_server, @@character_set_server, @@sql_mode,
|
|
35
|
+
@@lower_case_table_names, @@thread_stack, @@max_allowed_packet;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Compare the two outputs before concluding anything from a local run. (Machine-specific config
|
|
39
|
+
file locations and one developer's overrides are deliberately **not** recorded here — they belong
|
|
40
|
+
in per-developer notes, not the shared KB.)
|
|
41
|
+
|
|
42
|
+
## 1. `collation_server` — the ERROR 1267 trap is per-MACHINE, not prod-only
|
|
43
|
+
|
|
44
|
+
Measured: **prod `collation_server = utf8mb4_0900_ai_ci`** (the MySQL 8 *default*); **local
|
|
45
|
+
`utf8mb4_unicode_ci`** (an explicit config override on that machine).
|
|
46
|
+
|
|
47
|
+
So the difference is a **config property of whichever server was left at the MySQL 8 default** —
|
|
48
|
+
*not* a property of "production". Consequences:
|
|
49
|
+
|
|
50
|
+
- A developer on a **stock MySQL 8** install **will** reproduce `ERROR 1267 (Illegal mix of
|
|
51
|
+
collations)` locally, and is actively misled by any doc claiming it only happens on prod.
|
|
52
|
+
- A developer with the `utf8mb4_unicode_ci` override will **never** reproduce it.
|
|
53
|
+
- It is **fragile**: a MySQL reinstall or a reset config file silently flips the behavior with no
|
|
54
|
+
code change.
|
|
55
|
+
|
|
56
|
+
**Mitigation (unchanged, now correctly justified):** always pin
|
|
57
|
+
`CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` **explicitly** on temp/staging columns that
|
|
58
|
+
will be compared to a real table column. Because the server default is per-machine, **a local
|
|
59
|
+
pass proves nothing** about the deploy target. First hit on the AIG SaleItem load — see
|
|
60
|
+
[AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md) → *Uploading new
|
|
61
|
+
codes*.
|
|
62
|
+
|
|
63
|
+
## 2. `sql_mode` — an empty local mode truncates where prod throws ERROR 1406
|
|
64
|
+
|
|
65
|
+
MySQL 8's default `sql_mode` includes **`STRICT_TRANS_TABLES`** and **`ONLY_FULL_GROUP_BY`**. A
|
|
66
|
+
local instance configured with an **empty `sql_mode`**:
|
|
67
|
+
|
|
68
|
+
- **Silently TRUNCATES** an over-length write (e.g. a >255-char value into a `varchar(255)`)
|
|
69
|
+
where a strict server raises **`ERROR 1406 Data too long`**.
|
|
70
|
+
- Accepts `GROUP BY` queries that a strict (`ONLY_FULL_GROUP_BY`) server **rejects**.
|
|
71
|
+
|
|
72
|
+
Directly relevant to the 1.0 multi-email work: overflow of `Customers.emailAddress` behaves
|
|
73
|
+
**differently per machine** — truncate here, hard error there. See
|
|
74
|
+
[the library bridge doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
|
|
75
|
+
|
|
76
|
+
## 3. `lower_case_table_names` — a PERMANENT Windows-local vs Linux-prod difference
|
|
77
|
+
|
|
78
|
+
- Windows dev installs typically run **`lower_case_table_names=2`**; Linux prod runs **`0`**
|
|
79
|
+
(case-**sensitive** table names).
|
|
80
|
+
- It **cannot be changed after the data directory is initialized** — MySQL 8 refuses to start on a
|
|
81
|
+
data-dictionary mismatch — and `0` is **not permitted on Windows at all**. This one is not
|
|
82
|
+
fixable, only worked around.
|
|
83
|
+
- Consequence: SQL referencing a table with the wrong casing (e.g. `contactemailaddresses` instead
|
|
84
|
+
of `ContactEmailAddresses`) **passes locally and fails on prod.**
|
|
85
|
+
|
|
86
|
+
2.0 table names are **PascalCase**, so treat exact table-name casing as something local testing
|
|
87
|
+
can **never** validate. Review it by eye.
|
|
88
|
+
|
|
89
|
+
## 4. `thread_stack` — a bigger local stack hides ERROR 1436 until deploy
|
|
90
|
+
|
|
91
|
+
`thread_stack` is what produced the **`ERROR 1436 Thread stack overrun`** on the ~2,200-row
|
|
92
|
+
`UNION ALL` in TRUE-79534. A larger local stack means a big migration **parses locally and dies on
|
|
93
|
+
deploy** — the same shape as the collation trap. The fix is still structural (flat multi-row
|
|
94
|
+
`VALUES` into a temp table, never a long `UNION ALL` chain); see
|
|
95
|
+
[dbchanges2 architecture](../architecture.md) → *Bulk data loads*. Do not raise `thread_stack` for
|
|
96
|
+
a one-off load.
|
|
97
|
+
|
|
98
|
+
## Gotchas / known issues
|
|
99
|
+
|
|
100
|
+
- **The common failure mode is a false negative, not a false positive.** Every item above lets bad
|
|
101
|
+
SQL pass locally. Nothing here makes good SQL fail on prod.
|
|
102
|
+
- **Do not "fix" a parity gap by changing the server.** Pin collations in the SQL, keep casing
|
|
103
|
+
exact, size values to the column, and structure bulk loads to avoid parser recursion.
|
|
104
|
+
|
|
105
|
+
## Change history
|
|
106
|
+
|
|
107
|
+
- 2026-08-03 — Created. Established that the `ERROR 1267` temp-table collation trap is a
|
|
108
|
+
**per-machine config property, not a prod-only / version difference** (measured prod 8.0.39 with
|
|
109
|
+
`utf8mb4_0900_ai_ci`, local 8.0.44 with an explicit `utf8mb4_unicode_ci` override) — a developer
|
|
110
|
+
on a stock MySQL 8 install reproduces it locally. Added the `sql_mode` finding (empty local mode
|
|
111
|
+
silently truncates where strict prod raises `ERROR 1406`, and disables `ONLY_FULL_GROUP_BY`), the
|
|
112
|
+
permanent `lower_case_table_names` Windows-vs-Linux table-name-casing gap, the `thread_stack`
|
|
113
|
+
connection to `ERROR 1436`, and the cross-environment parity query. (mhammontree)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -12,7 +12,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
12
12
|
- **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
13
13
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
15
|
-
- **test** (Test) —
|
|
15
|
+
- **test** (Test) — 14 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
17
|
- **tools** (Tools) — 13 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 42 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 38 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 21 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 5 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -227,12 +227,15 @@ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Des
|
|
|
227
227
|
The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect
|
|
228
228
|
even if the run aborts). Do **not** raise `thread_stack` on the server for a one-off load.
|
|
229
229
|
5. **Pin the staging `partNumber` collation explicitly to `utf8mb4_unicode_ci`** (as shown
|
|
230
|
-
above). If you omit it, the temp column inherits the **server default
|
|
231
|
-
|
|
232
|
-
`utf8mb4_unicode_ci`. The anti-join's
|
|
233
|
-
collations and dies with **ERROR 1267
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
above). If you omit it, the temp column inherits the **server default**. On any server left at
|
|
231
|
+
the MySQL 8 default that is `utf8mb4_0900_ai_ci` — including **prod** — while
|
|
232
|
+
`Client_Aig.Items.partNumber` is `utf8mb4_unicode_ci`. The anti-join's
|
|
233
|
+
`existing.partNumber = src.partNumber` then mixes collations and dies with **ERROR 1267
|
|
234
|
+
(Illegal mix of collations)**. This is a **per-machine config** property, **not** a prod-only or
|
|
235
|
+
version difference (prod and local are both MySQL 8.0) — a developer on a stock MySQL 8 install
|
|
236
|
+
reproduces it locally, and a developer with a `utf8mb4_unicode_ci` override never does. So **a
|
|
237
|
+
local pass is not evidence.** See the gotcha and
|
|
238
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
236
239
|
|
|
237
240
|
## Client variations
|
|
238
241
|
|
|
@@ -252,7 +255,12 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
252
255
|
Both a flat/unknown key (a literal `UserDefined3`) and an array of bare strings
|
|
253
256
|
(`"contactEmailAddresses": ["a@b.com"]` instead of `[{"emailAddress": "a@b.com"}]`) are accepted
|
|
254
257
|
with **no error anywhere**. When verifying AIG's multi-email feed, assert **actual
|
|
255
|
-
`Client_Aig.ContactEmailAddresses` row counts** — never the HTTP status
|
|
258
|
+
`Client_Aig.ContactEmailAddresses` row counts** — never the HTTP status, **and (corrected
|
|
259
|
+
2026-08-03) never the 1.0 `TOGA_AIG.Customers.emailAddress` value either.** Proven by harness:
|
|
260
|
+
the bare-string shape does **not** blank the 1.0 field — `array_column()` returns an empty list,
|
|
261
|
+
the primary-email fallback fires, and the stored value is **byte-identical to correct
|
|
262
|
+
single-email output**, so "AIG sent the wrong shape" and "AIG hasn't enabled the feature yet"
|
|
263
|
+
are indistinguishable. The child row count is the **only** reliable signal. See
|
|
256
264
|
[api2 nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
257
265
|
- **A sale-item code missing from `Client_Aig.Items` currently produces an HTTP 500, not a clean
|
|
258
266
|
"Missing AIG item ID."** In prod (TRUE-79534) a valid-but-unknown code made `$itemId` empty and
|
|
@@ -295,17 +303,34 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
295
303
|
type. **Both stem from the same unguarded lookup** — `prePost` must guard an empty/failed lookup
|
|
296
304
|
and return a clean "Missing AIG item ID" **before** running the fulfillment query or injecting
|
|
297
305
|
anything. Not yet fixed.
|
|
298
|
-
- **Staging a temp table for a `Client_Aig` load can hit ERROR 1267 on
|
|
299
|
-
|
|
300
|
-
`
|
|
301
|
-
`
|
|
302
|
-
|
|
303
|
-
**
|
|
304
|
-
`
|
|
305
|
-
|
|
306
|
+
- **Staging a temp table for a `Client_Aig` load can hit ERROR 1267 — on whichever server is left
|
|
307
|
+
at the MySQL 8 default collation (a PER-MACHINE config trap, not "prod-only").** A
|
|
308
|
+
`CREATE TEMPORARY TABLE` column with no explicit collation inherits `@@collation_server`, which
|
|
309
|
+
is `utf8mb4_0900_ai_ci` by default on MySQL 8 (prod is exactly this) — while
|
|
310
|
+
`Client_Aig.Items.partNumber` is `utf8mb4_unicode_ci`; the anti-join `=` then mixes collations →
|
|
311
|
+
**ERROR 1267 (Illegal mix of collations)**. Measured 2026-08-03: prod Core **8.0.39 /
|
|
312
|
+
`utf8mb4_0900_ai_ci`**, local Core_2 **8.0.44 / `utf8mb4_unicode_ci` (an explicit override)** —
|
|
313
|
+
same engine, same major version, so this was **never** a version or environment difference. A
|
|
314
|
+
developer on a stock MySQL 8 install **will** reproduce it locally; one with the override never
|
|
315
|
+
will; a MySQL reinstall silently flips it. Always pin the staging `partNumber` column
|
|
316
|
+
`CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` (see *Uploading new codes* step 5) **and treat
|
|
317
|
+
a local pass as no evidence at all** — see
|
|
318
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
319
|
+
This is a general dbchanges2 temp-table-staging rule, not AIG-specific.
|
|
306
320
|
|
|
307
321
|
## Change history
|
|
308
322
|
|
|
323
|
+
- 2026-08-03 (later pass) — **Two corrections.** (1) The `ERROR 1267` temp-table collation trap is a
|
|
324
|
+
**per-machine server-config property, not a "prod-only" / version difference**: measured prod Core
|
|
325
|
+
8.0.39 with the MySQL 8 default `utf8mb4_0900_ai_ci` vs local Core_2 8.0.44 with an explicit
|
|
326
|
+
`utf8mb4_unicode_ci` override. The earlier reasoning ("the local server default is already
|
|
327
|
+
`utf8mb4_unicode_ci`") was wrong even though the mitigation was right — a developer on a stock
|
|
328
|
+
MySQL 8 install reproduces it locally, so **a local pass is not evidence.** New doc:
|
|
329
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
330
|
+
(2) The wrong-shaped-payload gotcha overstated the symptom: an array of **bare strings** does
|
|
331
|
+
**not** blank the 1.0 email field — it **silently degrades to primary-only**, producing output
|
|
332
|
+
byte-identical to correct single-email behavior, so it is undetectable from the stored value.
|
|
333
|
+
`Client_Aig.ContactEmailAddresses` row counts remain the only reliable signal. (mhammontree)
|
|
309
334
|
- 2026-08-03 — Review-only session (no code changed). **Business Unit (`UserDefined2`) is answered
|
|
310
335
|
and dropped**: AIG (2026-05-04) needs nothing returned — it is theirs, from the sale, for **sales**
|
|
311
336
|
invoicing (not claims invoicing); we asked them to omit it from the API call and **no invoice-file
|
package/package.json
CHANGED