Skip to content

metadata-protocol: SysMetadataRepository.listDrafts declares updatedAt: string | null and emits the raw updated_at column, so it hands a Date on Postgres/MySQL #14938

Description

@os-musk

Measured while implementing #14037. That card listed this site as a lower-confidence neighbour it could not resolve, and its triage ruled it explicitly out of scope with "file a separate card if it turns out to be real". It is real, and the declaration the neighbour note could not locate is named below. #14037 does not address it.

The site

packages/metadata-protocol/src/sys-metadata-repository.ts, in listDrafts, measured on origin/main at 431979e67:

1140      updatedAt: row.updated_at ?? row.created_at ?? null,

The declaration the #14037 note could not find

It is not a Zod schema — which is exactly why a schema search came up empty. It is an inline TypeScript return type on the method itself, in the same file:

1108      updatedAt: string | null;

(inside listDrafts's Promise<Array<{ type: string; name: string; organizationId: string | null; packageId: string | null; updatedAt: string | null; updatedBy: string | null }>>)

So the declared type is string | null, and rows is cast as any[] one line above the map, which is why tsc reports nothing. The ?? chain fires only on nullish, so a Date walks straight past it into the declared field.

Why the value is a Date on the live dialects

updated_at / created_at are BUILTIN audit columns. SqlDriver#formatOutput repairs them (repairNaiveUtcAuditTimestamp) only inside its if (this.isSqlite) arm, and withPostgresCalendarDayAsText leaves timestamptz / timestamp deliberately untouched because those are instants. Pinned live in packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts.

Severity, stated rather than assumed

Lower than #14037's three z.string().datetime() fields: this declaration is a plain string | null with no runtime refinement anywhere on the path, so there is no armed .parse() to trip. What it does today is hand a Date to a consumer whose type says string — the Studio "pending changes" list is the named consumer of this projection.

Suggested shape, not a decision

The same producer-side route #13997 and #14037 took: canonicalise at the adapter boundary that asserts the declared type. Two standing prohibitions from #13973 apply unchanged: no tolerant ?? fallback in a consumer, and no normalisation at the driver's read door.

One constraint worth carrying into whoever picks this up: the shared canonical-ISO spelling raises RangeError: Invalid time value on an Invalid Date, which #14078 measured to be reachable on both live dialects and which #13973 is blocked on. #14037 handled that by converting only a valid Date and passing every other shape through unchanged, so the fix imported neither answer to #14078; the same posture is available here.

Backlinks: #14037 (where this was measured, and which does not address it), #13973 (the census), #13997, #14078. None of them are addressed here.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions