toga-ai 1.0.668 → 1.0.669
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/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +28 -0
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +26 -4
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md +13 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/framework-branch-ahead-of-schema.md +256 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ related:
|
|
|
21
21
|
- ../workflows/environment-configuration-and-provisioning.md
|
|
22
22
|
- ../workflows/codepipeline-codeconnections-deploy.md
|
|
23
23
|
- ../../_underscore/features/togaiq-gateway-client.md
|
|
24
|
+
- ../../dbchanges2/workflows/framework-branch-ahead-of-schema.md
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
## Summary
|
|
@@ -154,6 +155,26 @@ Related detail: **the slug V2 uses to pick the override class is `Clients.client
|
|
|
154
155
|
(`V2.php:1521`), taken from the JWT. `Core.Clients` has **no `slug` column** — do not go looking for
|
|
155
156
|
one.
|
|
156
157
|
|
|
158
|
+
## ⚠ Moving a tier onto its correct branch is a SCHEMA event, not just a code event
|
|
159
|
+
|
|
160
|
+
The gotchas below are all about a tier running **stale** code. The inverse hurts too: when a deploy
|
|
161
|
+
moves an environment **forward** onto a newer `_underscore` branch — including a deploy that *fixes*
|
|
162
|
+
a wrong branch pin — it fast-forwards across every model change made since the pin, and
|
|
163
|
+
`_Model::initialize()` SELECTs **every declared property**. Any of those columns without a merged
|
|
164
|
+
`dbchanges2` migration becomes an **environment-wide MySQL 1054 → HTTP 500 / `EO-1`**, for every
|
|
165
|
+
tenant, immediately.
|
|
166
|
+
|
|
167
|
+
**Concrete (2026-08-26).** The 2026-08-26 deploy moved `sandbox-client` off the `_production`
|
|
168
|
+
framework — which it had been running for weeks (see the 2026-08-25 entry below) — onto
|
|
169
|
+
`_sandbox-client`, carrying ~2 weeks of model changes whose migrations were stranded on an unmerged
|
|
170
|
+
`dbchanges2` branch. Table-meta requests 500'd on `RecordFields.maxLength`, then (once that was
|
|
171
|
+
patched) on `TableViewFields.isGroupable`.
|
|
172
|
+
|
|
173
|
+
**Rule: pair a branch-pin correction with a migration sweep** — enumerate what the new branch expects
|
|
174
|
+
(`git log origin/_production..origin/<env-branch> --oneline -- Model/`) **before** redeploying, or
|
|
175
|
+
the tier trades a stale-code bug for a total outage. Full procedure:
|
|
176
|
+
[framework branch ahead of schema](../../dbchanges2/workflows/framework-branch-ahead-of-schema.md).
|
|
177
|
+
|
|
157
178
|
## Gotchas / known issues
|
|
158
179
|
|
|
159
180
|
- **⚠ Merging to the branch named after the EB environment can be a no-op.** Merge to
|
|
@@ -220,6 +241,13 @@ one.
|
|
|
220
241
|
|
|
221
242
|
## Change history
|
|
222
243
|
|
|
244
|
+
- 2026-08-27 — Recorded the **inverse hazard**: moving a tier **forward** onto its correct
|
|
245
|
+
`_underscore` branch is a **schema event**. The 2026-08-26 deploy moved `sandbox-client` off the
|
|
246
|
+
`_production` framework (which the 2026-08-25 entry below explains it had been running for weeks)
|
|
247
|
+
onto `_sandbox-client`, fast-forwarding it across ~2 weeks of model changes whose `dbchanges2`
|
|
248
|
+
migrations were stranded on an unmerged branch — producing environment-wide MySQL 1054 → 500/`EO-1`
|
|
249
|
+
on `RecordFields.maxLength` and then `TableViewFields.isGroupable`. A branch-pin correction must
|
|
250
|
+
ship with a migration sweep of `origin/_production..origin/<env-branch> -- Model/`. (apeterson)
|
|
223
251
|
- 2026-08-25 — Recorded the **second delivery mechanism**: besides `.ebextensions/git.php`, the
|
|
224
252
|
**CodePipeline `_underscore` source action has its own branch trigger configured in AWS**, and it
|
|
225
253
|
can serve the wrong branch while `git.<env>.json` is perfectly correct. Live case: `ApiSandboxClient`
|
|
@@ -43,9 +43,20 @@ it.
|
|
|
43
43
|
- **`Client_*.TableViewFields`** — one row per **column** of a view. Key columns: `recordFieldId`
|
|
44
44
|
(which field this column shows), `tableViewJoinId` (which join in the view the field is reached
|
|
45
45
|
through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden),
|
|
46
|
-
and the per-view **presentation flags** `isCopyable` / `isSortable` / `isFilterable
|
|
47
|
-
|
|
48
|
-
another.
|
|
46
|
+
and the per-view **presentation flags** `isCopyable` / `isSortable` / `isFilterable` /
|
|
47
|
+
`isGroupable`. These flags **are** per-view — the same underlying field can be copyable in one
|
|
48
|
+
view and not in another.
|
|
49
|
+
|
|
50
|
+
> **⚠ `isGroupable` is declared in the framework but has NO migration (as of 2026-08-27).**
|
|
51
|
+
> `_underscore` `9f296a71` (2026-08-26) added `public $isGroupable = self::FIELD_BOOLEAN;` to
|
|
52
|
+
> `_Model_Client_TableViewField` **and** named the column in `_Model_Client_TableView::meta()`'s
|
|
53
|
+
> hand-written `SELECT` — but no `dbchanges2` file adds it, in any branch or in the blank client
|
|
54
|
+
> template. Any environment whose framework branch carries that commit **500s (`EO-1`, MySQL 1054)
|
|
55
|
+
> on every table-meta request, for every tenant**, until the column is added to each `Client_*` DB.
|
|
56
|
+
> It was hand-applied to `sandbox-client` as
|
|
57
|
+
> `TINYINT UNSIGNED NULL … AFTER isFilterable` (matching its sibling flags); the real fan-out
|
|
58
|
+
> migration is still outstanding. Background:
|
|
59
|
+
> [framework branch ahead of schema](../../dbchanges2/workflows/framework-branch-ahead-of-schema.md).
|
|
49
60
|
|
|
50
61
|
### A column's `type` / `precision` are NOT per-view — they come from `Core.RecordFields`
|
|
51
62
|
|
|
@@ -207,9 +218,20 @@ SalesOrderListingSortByDateOrderDesc.sql`).
|
|
|
207
218
|
is suppressed.
|
|
208
219
|
- **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields`
|
|
209
220
|
changes every view/client that shows the field; there is no per-view type override. `isCopyable`,
|
|
210
|
-
`isSortable`, `isFilterable`, `isVisible` are the per-view knobs (on
|
|
221
|
+
`isSortable`, `isFilterable`, `isGroupable`, `isVisible` are the per-view knobs (on
|
|
222
|
+
`TableViewFields`).
|
|
223
|
+
- **`TableView::meta()` contains a hand-written `SELECT` that names `TableViewFields` columns
|
|
224
|
+
explicitly.** So a new column on that table is required by **two** mechanisms — the model property
|
|
225
|
+
*and* that query — and a grep for the property alone under-reports what a missing column breaks.
|
|
211
226
|
|
|
212
227
|
## Change history
|
|
228
|
+
- 2026-08-27 - Added **`isGroupable`** as a fourth per-view presentation flag on `TableViewFields`,
|
|
229
|
+
with the warning that it is declared in `_underscore` (`9f296a71`, 2026-08-26 — model property
|
|
230
|
+
**and** `TableView::meta()`'s hand-written SELECT) while **no `dbchanges2` migration adds it on any
|
|
231
|
+
branch**, so any environment on that framework branch 500s (`EO-1`/1054) on every table-meta
|
|
232
|
+
request for every tenant. Hand-applied to `sandbox-client` as `TINYINT UNSIGNED NULL AFTER
|
|
233
|
+
isFilterable`; fan-out migration still outstanding. Also recorded that `meta()`'s explicit SELECT
|
|
234
|
+
makes a property grep an under-count of a missing column's blast radius. (apeterson)
|
|
213
235
|
- 2026-08-26 - Added the **per-client type override**: a column can point at
|
|
214
236
|
`TableViewFields.customRecordFieldId` instead of `recordFieldId`, so registering a
|
|
215
237
|
`Client_<X>.CustomRecordFields` row with the **same field name** as the Core RecordField and the
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| [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/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, 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, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql |
|
|
8
8
|
| [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/ |
|
|
9
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
10
|
+
| [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
|
|
10
11
|
| [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**. | |
|
|
11
12
|
| [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/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Aig/_modules.txt, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql |
|
|
12
13
|
| [Verifying whether a dbchanges2 migration actually ran in an environment](workflows/verifying-a-migration-ran.md) | dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered from the **rows the file would have produced**. | Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql |
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-11
|
|
10
|
-
owners: ["bala", "mhammontree"]
|
|
10
|
+
owners: ["bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Growrk/2026-05-28.sql
|
|
13
13
|
- dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql
|
|
@@ -22,6 +22,7 @@ related:
|
|
|
22
22
|
- ../../_underscore/features/acl-permission-chain.md
|
|
23
23
|
- ../../api2/features/v2-api-error-codes.md
|
|
24
24
|
- ../../../../clients/growrk/features/units-netsuite-custom-fields.md
|
|
25
|
+
- ./framework-branch-ahead-of-schema.md
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
## Summary
|
|
@@ -36,6 +37,12 @@ touches the record.
|
|
|
36
37
|
> (works in prod, broken in beta). Here **production is the outlier** and beta/dev-sandbox is
|
|
37
38
|
> correct — so "it works on beta" is not evidence the schema is fine. Check *which* side is behind
|
|
38
39
|
> before assuming.
|
|
40
|
+
>
|
|
41
|
+
> **Third possibility — no database is behind, the CODE is ahead.** If the 1054 started right after
|
|
42
|
+
> a **deploy**, is **not** client-specific, and the column exists in **no** environment, the
|
|
43
|
+
> framework branch is running ahead of schema (its migration was never merged). Use
|
|
44
|
+
> [framework branch ahead of schema](./framework-branch-ahead-of-schema.md) — the repair is a branch
|
|
45
|
+
> merge, not a per-client catch-up file.
|
|
39
46
|
|
|
40
47
|
Written from repairing `Client_Growrk.Units` (missing `c_lastModified`, 6 of 7 columns and 1 of 6
|
|
41
48
|
metadata rows applied) — see
|
|
@@ -155,6 +162,11 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
|
|
|
155
162
|
|
|
156
163
|
## Change history
|
|
157
164
|
|
|
165
|
+
- 2026-08-27 — Added a pointer to the **third drift direction**: when the 1054 starts right after a
|
|
166
|
+
deploy, is not client-specific, and the column exists in no environment, the **framework branch is
|
|
167
|
+
ahead of schema** and the repair is a branch merge, not a catch-up file — see
|
|
168
|
+
[framework branch ahead of schema](./framework-branch-ahead-of-schema.md). No change to this
|
|
169
|
+
procedure. (apeterson)
|
|
158
170
|
- 2026-08-11 — TRUE-80824: **corrected the premise.** The `Client_Growrk` drift was **not** a partially
|
|
159
171
|
applied module migration — `Client_Growrk/2026-05-28.sql` is committed on `_main` (`af30e63`) and was
|
|
160
172
|
**never executed** against production, which explains both the 2026-08-10 `Units` 500s and the
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-27
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Core/RecordField.php
|
|
13
|
+
- _underscore/Model/Client/TableViewField.php
|
|
14
|
+
- _underscore/Model/Client/TableView.php
|
|
15
|
+
- _underscore/Model/Core/Surface.php
|
|
16
|
+
- _underscore/Model/Client/Urgency.php
|
|
17
|
+
- api2/Component/Api/V2/V2.php
|
|
18
|
+
- dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql
|
|
19
|
+
- dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql
|
|
20
|
+
- dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql
|
|
21
|
+
- dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql
|
|
22
|
+
related:
|
|
23
|
+
- ./client-schema-drift-audit.md
|
|
24
|
+
- ./nonprod-metadata-drift-repair.md
|
|
25
|
+
- ./verifying-a-migration-ran.md
|
|
26
|
+
- ../architecture.md
|
|
27
|
+
- ../../api2/features/environment-variable-drives-underscore-branch.md
|
|
28
|
+
- ../../api2/features/tableview-field-metadata.md
|
|
29
|
+
- ../../_underscore/features/field-storage-row-hydration.md
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Summary
|
|
33
|
+
|
|
34
|
+
A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database
|
|
35
|
+
went backwards — **the code went forwards**. A deploy moved an environment onto a newer
|
|
36
|
+
`_underscore` branch that carries model changes whose `dbchanges2` migrations were **never merged**.
|
|
37
|
+
Every request touching an affected model then dies with MySQL **1054 "Unknown column"** →
|
|
38
|
+
**HTTP 500 / `EO-1`**, for **every tenant at once**, tenant-agnostic.
|
|
39
|
+
|
|
40
|
+
> **Which of the three drift docs do you want?**
|
|
41
|
+
>
|
|
42
|
+
> | The outlier is… | Doc |
|
|
43
|
+
> |---|---|
|
|
44
|
+
> | one **client's** DB, behind its models (migration partially/never applied) | [client schema-drift audit](./client-schema-drift-audit.md) |
|
|
45
|
+
> | a **non-prod environment's** metadata, behind production | [non-prod metadata drift repair](./nonprod-metadata-drift-repair.md) |
|
|
46
|
+
> | **the code** — the framework branch is ahead of every schema, including production's | **this doc** |
|
|
47
|
+
>
|
|
48
|
+
> The tell for *this* doc: the failure started **immediately after a deploy**, it is **not**
|
|
49
|
+
> client-specific, and the missing column exists in **no** environment because the migration
|
|
50
|
+
> exists in **no merged branch**.
|
|
51
|
+
|
|
52
|
+
Written from the 2026-08-26 `sandbox-client` outage: table-meta requests returned 500/EO-1 on two
|
|
53
|
+
successive columns after a deploy moved that environment from the `_production` framework onto
|
|
54
|
+
`_sandbox-client`, which carried ~2 weeks of model changes.
|
|
55
|
+
|
|
56
|
+
## Why one model property takes down a whole environment
|
|
57
|
+
|
|
58
|
+
`_Model::initialize()` builds its SELECT from **every property declared on the class**, ignoring the
|
|
59
|
+
caller's `fields=` (see
|
|
60
|
+
[client schema-drift audit](./client-schema-drift-audit.md) → *Why it 500s*).
|
|
61
|
+
So this one line, shipped without a migration, breaks **every construction of that model,
|
|
62
|
+
everywhere**:
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
public $isGroupable = self::FIELD_BOOLEAN; // _Model_Client_TableViewField
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Second variant, same failure, no model property involved:** hand-written SQL inside a model that
|
|
69
|
+
names the new column explicitly. `_Model_Client_TableView::meta()` lists `isGroupable` in its own
|
|
70
|
+
`SELECT`, so the column is required **twice** by two different mechanisms — a grep for the property
|
|
71
|
+
alone under-reports the blast radius.
|
|
72
|
+
|
|
73
|
+
**Contrast — `FIELD_STORAGE` is the exception**: those columns are excluded from the main SELECT and
|
|
74
|
+
hydrated lazily per row, so they 500 only once a query **returns** rows. See
|
|
75
|
+
[FIELD_STORAGE row hydration](../../_underscore/features/field-storage-row-hydration.md).
|
|
76
|
+
|
|
77
|
+
**Why a deploy can cause this without anyone changing the framework:** api2 clones `_underscore` at
|
|
78
|
+
**build time from a moving `_<ENVIRONMENT>` branch**, and the CodePipeline source action has its own
|
|
79
|
+
branch setting. A redeploy — including one that *fixes* a stale box — can silently move the
|
|
80
|
+
environment onto a newer framework branch. See
|
|
81
|
+
[ENVIRONMENT decides the `_underscore` branch](../../api2/features/environment-variable-drives-underscore-branch.md).
|
|
82
|
+
|
|
83
|
+
> **The corollary that surprises people: "unstaling" a box is a schema event.** Correcting a wrong
|
|
84
|
+
> branch pin fast-forwards the environment across every model change since the pin was made. Before
|
|
85
|
+
> re-pointing a tier at its correct branch, run the sweep below — the branch fix and the migrations
|
|
86
|
+
> ship together, or the tier trades a stale-code bug for an environment-wide 500.
|
|
87
|
+
|
|
88
|
+
## Steps
|
|
89
|
+
|
|
90
|
+
1. **Get the real exception.** You need the **column name**, the **database** (Core vs Client), and
|
|
91
|
+
the **model file + line** from the stack. Both 2026-08-26 failures pointed at the same entry
|
|
92
|
+
point, `V2.php:4516` → `_Model_Client_TableView::meta()`, but at different columns in different
|
|
93
|
+
databases — so the entry point tells you nothing; the column does.
|
|
94
|
+
|
|
95
|
+
2. **Prove the code is ahead, not the DB behind** — in `_underscore`:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
git log --all -S"<columnName>" -- Model/ # which commit introduced the property
|
|
99
|
+
git branch -a --contains <sha> # WHICH environment branches carry it
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If the commit is on `_sandbox-client` but not `_production`, the environments are running
|
|
103
|
+
**different schema expectations** from the same repo. That is the finding.
|
|
104
|
+
|
|
105
|
+
3. **Find out whether a migration exists at all** — in `dbchanges2`:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
git log --all -S"<columnName>" # any branch, ever
|
|
109
|
+
git ls-tree --name-only origin/_main <folder>/ # is it merged?
|
|
110
|
+
git merge-base --is-ancestor <sha> origin/_main
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Three possible answers, and they need different fixes: **merged** (an executor problem — use the
|
|
114
|
+
other two drift docs), **written but stranded on an unmerged branch** (merge it), or **never
|
|
115
|
+
written** (author it).
|
|
116
|
+
|
|
117
|
+
4. **⚠ Do the full sweep BEFORE fixing anything.** This is the step that turns the incident from
|
|
118
|
+
whack-a-mole into one piece of work:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
git log origin/_production..origin/<env-branch> --oneline -- Model/
|
|
122
|
+
git show <sha> -- Model/ | grep -E "^\+\s*public \$"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Enumerate **every** new column the framework branch expects, then cross-check each token against
|
|
126
|
+
`dbchanges2`. Also grep the same commits for hand-written `SELECT`s (the `meta()` variant above),
|
|
127
|
+
which the `public $` grep misses.
|
|
128
|
+
|
|
129
|
+
5. **Merge the stranded branch — do not hand-patch column by column.** *(Decision, 2026-08-27.)*
|
|
130
|
+
The failures arrive **one at a time**: each next column only surfaces once the previous is fixed,
|
|
131
|
+
so column-by-column patching guarantees a repeat incident per column. And a column applied by
|
|
132
|
+
hand exists in **no migration file**, so that environment drifts permanently from every other one.
|
|
133
|
+
Merge the branch, then verify with [verifying a migration ran](./verifying-a-migration-ran.md).
|
|
134
|
+
|
|
135
|
+
6. **If you must unblock by hand, write down exactly what you ran** (see the section below) and
|
|
136
|
+
treat the eventual migration as **idempotent by requirement**, not by preference — it will run
|
|
137
|
+
against at least one database that already has the column.
|
|
138
|
+
|
|
139
|
+
7. **Remember `Client/` is a fan-out.** A Core column is one `ALTER`; a Client column is one `ALTER`
|
|
140
|
+
**per tenant database**. Getting this wrong fixes the tenant you are testing and leaves the rest
|
|
141
|
+
500ing.
|
|
142
|
+
|
|
143
|
+
## The 2026-08-26 `sandbox-client` incident (worked example)
|
|
144
|
+
|
|
145
|
+
Two successive 500s, both `V2.php:4516` → `_Model_Client_TableView::meta()`:
|
|
146
|
+
|
|
147
|
+
| # | Error | DB | Failing path | Mechanism |
|
|
148
|
+
|---|---|---|---|---|
|
|
149
|
+
| 1 | `Unknown column 'maxLength' in 'field list'` (`SELECT … FROM RecordFields WHERE id = 41`) | **Core** | `Model/Client/TableView.php:84` → `_Model::initialize()` → `Query.php:405` | model **property** (`_Model_Core_RecordField::$maxLength`) |
|
|
150
|
+
| 2 | `Unknown column 'TableViewFields.isGroupable' in 'field list'` | **Client** | `Model/Client/TableView.php:188` → `_Query::fetchRow()` → `Query.php:186` | **hand-written SELECT** in `meta()` |
|
|
151
|
+
|
|
152
|
+
The second only appeared **after** the first was fixed — the canonical reason step 4 exists.
|
|
153
|
+
|
|
154
|
+
## Current drift inventory — `_underscore` `_sandbox-client` vs. schema (as of 2026-08-27)
|
|
155
|
+
|
|
156
|
+
Framework commits ahead of `_production` on `_sandbox-client`: `af90e987` (08-13), `a5e79448`
|
|
157
|
+
(08-14), `9f296a71` (08-26), `78ad65a5` (08-26), `64233162` (08-26 — logic only, no new columns).
|
|
158
|
+
|
|
159
|
+
**Category A — migration WRITTEN but stranded on `dbchanges2` branch `origin/toga25-desk`**, never
|
|
160
|
+
merged to `origin/_main`, so it has run in **no** environment fed by `_main`:
|
|
161
|
+
|
|
162
|
+
| Object | File (on `origin/toga25-desk`) |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `Core.RecordFields.maxLength` + `validationType` | `Core/2026-08-14c - RecordFieldsValidationMetadata.sql` (commit `84a022c`, jcardinal, 2026-08-17) |
|
|
165
|
+
| `Client.RecordFieldSettings` (new table) | `Client/2026-08-14a - RecordFieldSettingsClientTable.sql` |
|
|
166
|
+
| `Client.UserNavPreferences` (new table) | `Client/2026-08-13a`, `b`, `d`, `e`, `g`, `h` (commit `9523c04`) |
|
|
167
|
+
| `Client.PersonaNavVisibility` (new table) | `Client/2026-08-13c`, `f` (same commit) |
|
|
168
|
+
| Core surface/ACL seeds | `Core/2026-08-14a` SettingsNavSurfaceSeed, `2026-08-14b` SettingsProfileAndAppearance, `2026-08-14d` ProfileSensitiveFieldAclRevoke, `2026-08-14e` SettingsProfileFieldElements, `2026-08-14f` LogoutConfirmSurfaceSeed |
|
|
169
|
+
|
|
170
|
+
`Core/2026-08-14c` adds `maxLength INT UNSIGNED NULL` + `validationType VARCHAR(32) NULL` and then
|
|
171
|
+
seeds **13 platform defaults by literal `RecordFields` id** (113, 115–118, 1324–1327, 1329, 1863).
|
|
172
|
+
`Client.RecordFieldSettings.recordFieldId` is a deliberate **soft (non-FK) cross-cluster reference**
|
|
173
|
+
to `Core.RecordFields` — correct per the [database-isolation rule](../architecture.md), not an
|
|
174
|
+
oversight.
|
|
175
|
+
|
|
176
|
+
**Category B — framework shipped with NO migration written ANYWHERE** (no `dbchanges2` branch, not in
|
|
177
|
+
`Client/2026-06-03- BLANK_CLIENT_DATABASE.sql`). Both landed **2026-08-26**, the day of the deploy:
|
|
178
|
+
|
|
179
|
+
- **`Client.TableViewFields.isGroupable`** — `_underscore` `9f296a71` "Add groupable table view
|
|
180
|
+
fields". Adds the model property **and** the column to `meta()`'s hand-written SELECT. Verified
|
|
181
|
+
absent from every `dbchanges2` branch (`git log --all -S"isGroupable"` returns nothing).
|
|
182
|
+
- **`Client.Urgencies.slug`** — `_underscore` `78ad65a5` "Add slug to urgency model". The blank
|
|
183
|
+
template's `Urgencies` has only `id`/`uuid`/`weight`/`name`. **Has not surfaced yet** because
|
|
184
|
+
Supply never constructs an Urgency — it is the Desk/Tickets path, so this one is a **pre-detonated
|
|
185
|
+
mine**, not a live outage.
|
|
186
|
+
|
|
187
|
+
**Verified NOT a problem** (already on `_main`): the `SurfaceElements` columns
|
|
188
|
+
`showRequiredIndicator`, `isSearchable`, `characterLimit`, `trueMessageId`, `falseMessageId`,
|
|
189
|
+
`inputType`, `validationType` — `Core/2026-07-17a - Migrate - AlterSurfaceElements.sql`.
|
|
190
|
+
|
|
191
|
+
> **Note on the stranded branch's file names:** `origin/toga25-desk` carries **three**
|
|
192
|
+
> `Core/2026-08-14a` files and **three** `Client/2026-08-14a` files. That violates the
|
|
193
|
+
> one-letter-per-day contract in [architecture.md](../architecture.md) and is the same shape as the
|
|
194
|
+
> unproven duplicate-letter executor lead in
|
|
195
|
+
> [non-prod metadata drift](./nonprod-metadata-drift-repair.md). Fix the letters as part of the merge.
|
|
196
|
+
|
|
197
|
+
## Hand-applied to `sandbox-client` on 2026-08-27 (NOT in any migration file)
|
|
198
|
+
|
|
199
|
+
Recorded so the real migrations are written **idempotently** and nobody is surprised by a column that
|
|
200
|
+
exists here and in no `.sql` file.
|
|
201
|
+
|
|
202
|
+
**Core (single DB)** — lifted verbatim from the stranded `Core/2026-08-14c`; the 13 seed `UPDATE`s
|
|
203
|
+
were **not** run, because `NULL` correctly means "no platform default":
|
|
204
|
+
|
|
205
|
+
```sql
|
|
206
|
+
ALTER TABLE RecordFields
|
|
207
|
+
ADD COLUMN maxLength INT UNSIGNED NULL COMMENT 'Platform-default maximum character length for the field',
|
|
208
|
+
ADD COLUMN validationType VARCHAR(32) NULL COMMENT 'Named front-end validator, e.g. email/phone';
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Client_<Tenant> (FAN-OUT — every tenant DB)** — no migration file exists for this yet; the
|
|
212
|
+
type/nullability/position were chosen to match its siblings `isSortable`/`isFilterable`/
|
|
213
|
+
`isCopyable`/`isEditable` in the blank template:
|
|
214
|
+
|
|
215
|
+
```sql
|
|
216
|
+
ALTER TABLE TableViewFields
|
|
217
|
+
ADD COLUMN isGroupable TINYINT UNSIGNED NULL COMMENT 'Whether or not the field is groupable' AFTER isFilterable;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Still outstanding in `sandbox-client` at session end:** the 13 Core seed `UPDATE`s,
|
|
221
|
+
`Client.RecordFieldSettings`, `Client.Urgencies.slug`, `Client.PersonaNavVisibility`,
|
|
222
|
+
`Client.UserNavPreferences`, and the five Core `2026-08-14` surface/ACL seeds.
|
|
223
|
+
|
|
224
|
+
## Gotchas / known issues
|
|
225
|
+
|
|
226
|
+
- **The failures are serialized — one fix reveals the next.** Never treat "the 500 is gone" as
|
|
227
|
+
"the environment is healthy" until the step-4 sweep comes back empty.
|
|
228
|
+
- **`git grep` for the property under-counts.** A column can be required by a hand-written `SELECT`
|
|
229
|
+
inside a model with no property declared at all (`TableView::meta()`), and by both at once.
|
|
230
|
+
- **A column that has not failed yet is not safe** — it is waiting for the first request that
|
|
231
|
+
constructs that model. `Urgencies.slug` is live in `sandbox-client` right now and simply
|
|
232
|
+
unreachable from Supply.
|
|
233
|
+
- **Fixing a wrong branch pin is a schema change in disguise.** Pair the branch correction with the
|
|
234
|
+
migration sweep; do not ship it as a lone "deploy fix".
|
|
235
|
+
- **A migration on an unmerged branch has run nowhere** — including production. "It's written" and
|
|
236
|
+
"it's applied" are different claims, and so are "it's on a branch" and "it's on `_main`".
|
|
237
|
+
- **Core is one `ALTER`; `Client` is one per tenant.** Fixing only the tenant in front of you leaves
|
|
238
|
+
every other tenant 500ing on the same request.
|
|
239
|
+
|
|
240
|
+
## Change history
|
|
241
|
+
|
|
242
|
+
- 2026-08-27 — Created from the `sandbox-client` outage that followed the 2026-08-26 production
|
|
243
|
+
`_underscore`/`api2`/`_production`-DB deploy. Recorded the **third drift direction** (the code
|
|
244
|
+
moved forward, not the DB backward): a deploy moved the environment off the `_production`
|
|
245
|
+
framework onto `_sandbox-client`, which carried ~2 weeks of model changes whose migrations were
|
|
246
|
+
never merged, producing environment-wide MySQL 1054 → 500/`EO-1`. Documented the two mechanisms
|
|
247
|
+
(a `public $x = self::FIELD_*` property, which `_Model::initialize()` puts in every SELECT; and a
|
|
248
|
+
hand-written `SELECT` inside a model such as `TableView::meta()`), the reusable triage
|
|
249
|
+
(`git log --all -S"<column>"` + `git branch -a --contains` in `_underscore`, the same in
|
|
250
|
+
`dbchanges2`, then the `git log origin/_production..origin/<env-branch> -- Model/` sweep), and the
|
|
251
|
+
**decision to merge the stranded branch wholesale rather than hand-patch column by column** —
|
|
252
|
+
because failures serialize and hand-applied columns drift permanently. Captured the full
|
|
253
|
+
2026-08-27 inventory (Category A stranded on `origin/toga25-desk`: `RecordFields.maxLength`/
|
|
254
|
+
`validationType`, `RecordFieldSettings`, `UserNavPreferences`, `PersonaNavVisibility`, five Core
|
|
255
|
+
surface/ACL seeds; Category B never written anywhere: `TableViewFields.isGroupable`,
|
|
256
|
+
`Urgencies.slug`) and the two `ALTER`s hand-applied to `sandbox-client`. (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 69 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 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)_ — 13 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
package/package.json
CHANGED