Skip to content

platform-objects' keyed-text-bounds pin cannot reach plugin/service packages — four keyed unbounded text columns in plugin-audit and plugin-security still break MySQL schema-sync #12059

Description

@os-warren

Found while landing route A's last column for #11374 (PR #12058). Filed unassigned.

What

The #11374 route-A rule is "a text-family column that a declared index keys on
must declare a maxLength, because a bound is what lets the column be a key at
all". On MySQL an unbounded keyed text column is emitted TEXT, the
ALTER TABLE ... ADD INDEX is refused with ER_BLOB_KEY_WITHOUT_LENGTH, and
the object lands registered-but-broken with its declared index silently absent.

That rule is now enforced by a pin — but the pin lives in
@objectstack/platform-objects and enumerates only the objects that package
exports. Platform objects that moved out to plugins under ADR-0029 K2 are
therefore outside it, and four of them declare keyed text columns with no bound:

ObjectColumnDeclarationKeyed by
sys_activityrecord_idpackages/plugins/plugin-audit/src/objects/sys-activity.object.ts:176{ fields: ['object_name', 'record_id'] } (:262)
sys_audit_logrecord_idpackages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts:229{ fields: ['object_name', 'record_id'] } (:334)
sys_audience_binding_suggestionpackage_idpackages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts:44(package_id, permission_set_name, anchor)
sys_audience_binding_suggestionpermission_set_namesame filesame composite

All four are Field.text({ ... }) with no maxLength key in the declaration.

Evidence, and what is NOT claimed

Declaration-level, measured by walking every *.object.ts outside
packages/platform-objects/ and intersecting each object's indexes with its
text-family fields.
Each row above was then read by hand to confirm it is a
real Field.text with no maxLength and a real index entry naming it.

The mechanism is measured live on this exact code path, on the same driver,
in PR #12058: sys_import_job.created_by, an unbounded keyed Field.text,
produced

[sql-driver] cannot create index 'idx_sys_import_job_created_by_created_at'
on "sys_import_job" — MySQL refuses a TEXT/BLOB column in a key without a key length

against live MySQL 8.0.46, with the column reading back text /
CHARACTER_MAXIMUM_LENGTH 65535 from information_schema and its declared
index absent. Declaring the bound moved it to varchar(255) and the index
appeared.

What I did not do: run these four objects through a live syncSchema.
They are not reachable from their packages' export surface — plugin-audit's
package.json declares only the . export and its root barrel does not
re-export ./objects, so a probe cannot import SysActivity at all. Whoever
picks this up should drive them through the plugin's own registration path
rather than by importing the object, and should treat the counts as unverified
until they do.

Why it matters (declared ≠ enforced)

Two separate failures, and the second is the one worth fixing:

  1. Four declared indexes cannot exist on MySQL, so those objects are
    registered-but-broken there — sys_audit_log and sys_activity lose the
    (object_name, record_id) lookup that the ActivityPointer read path
    (ADR-0052 §5) assumes.
  2. The pin is scoped to a package, not to the defect class. This is the
    second time that exact shape has bitten: route A's original pin was scoped to
    identity/ and that is precisely how sys_import_job.created_by in audit/
    survived the first pass. PR feat(platform-objects): declare a sourced maxLength on sys_import_job.created_by (#11374 route A, last column) #12058 widened it from identity/ to the whole
    of platform-objects (20+ objects → 45, 30+ keyed text columns → 78), but
    the package boundary is still a boundary, and objects keep moving across it
    under ADR-0029.

Direction (for triage — not prescribing a fix)

  • Declare a sourced bound on each of the four, in route A's shape (every bound
    derived from a named producer, stated so it is vetoable in review). For the
    two record_id columns the obvious source is referenced-column transitivity
    from the record id they hold, exactly as feat(platform-objects): declare a sourced maxLength on sys_import_job.created_by (#11374 route A, last column) #12058 did for created_by; the
    physical id column driver-sql creates is table.string('id').primary(),
    knex's varchar(255).
  • And/or lift the pin so it walks every package that ships platform objects
    rather than one package — the class-level fix, and the one that stops this
    recurring the next time an object moves.

Related: #11374 (route A), PR #11699 (13 identity columns), PR #12058 (this
card's origin), #11627 (the separate >768-character key class).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions