Skip to content

service-messaging: 15 keyed text columns declare no maxLength — five sys_notification_* objects land registered-but-broken on MySQL #12978

Description

@os-elon

Found by the class-level gate built for #12147 (node scripts/check-keyed-text-bounds.mjs), on fa5d137ab0. This is a live member set of the #11374 route-A class, in a package that no existing pin covers — packages/services/service-messaging has no keyed-text-bounds pin, and the three that exist are scoped to platform-objects, plugin-audit and plugin-security.

The mechanism, restated once

A text-family column a declared index keys on must declare a maxLength, because a bound is what lets the column be a key at all. Without one, driver-sql's keyableTextLength returns null and createColumn emits table.text(name). MySQL then refuses ALTER TABLE … ADD [UNIQUE] INDEX on a TEXT column with no prefix length (ER_BLOB_KEY_WITHOUT_LENGTH), so CREATE TABLE succeeds, the index does not, and the object lands registered-but-broken with its declared index silently absent. Measured live on MySQL 8.0.46 in PR #12058.

The severity is not uniform across the 15. sys_notification_delivery declares

{ fields: ['notification_id', 'recipient_id', 'channel'], unique: true }

over three unbounded text columns. That index is the outbox's dedup constraint ("one delivery per (event, recipient, channel)"). On MySQL it does not exist, so the constraint the comment describes is not enforced and duplicate deliveries are admitted.

The 15 columns

columntypeindex kindplausible referent for the bound
sys_notification_delivery.notification_idtextUNIQUE (composite) + plainFK to sys_notification.id
sys_notification_delivery.recipient_idtextUNIQUE (composite)FK to sys_user.id
sys_notification_delivery.channeltextUNIQUE (composite)channel id vocabulary
sys_notification_delivery.digest_keytextplain (composite)derived `recipient
sys_notification_preference.user_idtextplainFK to sys_user.id, or the literal *
sys_notification_preference.topictextplaintopic vocabulary, or *
sys_notification_preference.channeltextplainchannel id vocabulary, or *
sys_notification_receipt.notification_idtextplainFK to sys_notification.id
sys_notification_receipt.user_idtextplainFK to sys_user.id
sys_notification_receipt.channeltextplainchannel id vocabulary
sys_notification_subscription.topictextplaintopic vocabulary
sys_notification_subscription.principaltextplainRecipientResolver.resolveOne() accepts an email-shaped value as well as a bare user id (#9807)
sys_notification_template.topictextplaintopic vocabulary
sys_notification_template.channeltextplainchannel id vocabulary
sys_notification_template.localetextplainBCP-47 tag; sys_email_template.locale is bounded at 16

Reproduce the list with node scripts/check-keyed-text-bounds.mjs --list once the #12147 gate lands.

Why this is filed rather than fixed on #12147's PR

#12147 is a domain:devx card whose deliverable is the gate. Bounding these columns is not a mechanical drive-by:

  1. Each bound needs a sourced producer. Route A's shape is a NAMED producer stated in the declaration so it is vetoable in review. topic, channel and principal have no bound that can be read off a sibling declaration — channel and topic are open string vocabularies here, and principal deliberately accepts an email-shaped value. Picking a number without sourcing it is the failure route A exists to prevent.
  2. It is a schema migration, not just a declaration. These columns are emitted TEXT today. Declaring a bound moves them to varchar(n), which is a drift op against every existing deployment, and the narrowing direction is categorised destructive in schema-drift.ts. That disposition belongs to the services/engine lane with a changeset, not to a devx gate PR.
  3. The two id-family columns are the easy half and the rest are not, so a partial fix would leave the object's composite UNIQUE index still inexpressible while reading as addressed.

Until it is fixed, all 15 are recorded in the gate's per-package ALLOWLIST as kind: 'pending' rows citing this issue. That ledger is enumerated, not a wildcard: a new unbounded keyed text column anywhere in the tree still fails the gate, a row whose column gets bounded fails as stale, and a row whose column stops being keyed text fails as stale. Nothing here goes quiet.

Not in scope of this one

The separate >768-character key class (#11627 for the UNIQUE half, #11701 for the non-unique half) is a different predicate with a different disposition and is not folded in. None of the 15 declares a bound at all, so they are all route-A members first.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions