Skip to content

@happyvertical/sql: Postgres upsert fails when a column uses a reserved identifier #1037

@willgriffin

Description

@willgriffin

Summary

@happyvertical/sql Postgres upsert() appears to emit unquoted column identifiers. A persisted field named end generated invalid SQL during a SMRT backfill, because Postgres parses end as syntax instead of a column identifier.

Observed

Running pnpm --filter @willgriffin/site db:migrate in willgriffin.dev failed while backfilling a SMRT object with table resume_positions and field end:

DatabaseError: Database query failed: UPSERT INTO resume_positions
Cause: syntax error at or near "end", code=42601, severity=ERROR

The failing value payload included:

{
  table: "resume_positions",
  values: {
    position_id: "happy-vertical",
    role: "Founder & Lead Engineer",
    company: "happyvertical.com",
    start: "Dec 2024",
    end: "Present",
    sort_order: 0
  },
  conflictColumns: ["slug", "context"]
}

Expected

CRUD helpers should safely quote/escape generated identifiers for the target adapter, or reject invalid/reserved identifiers before query execution with a clear error. A column named end should not produce invalid Postgres SQL.

Impact

Any SMRT object or direct @happyvertical/sql caller with fields mapped to reserved SQL words can fail at runtime during insert/upsert even if schema generation succeeds. The local workaround was to rename the SMRT field from end to endLabel, but the package should make generated CRUD SQL robust.

Environment

  • @happyvertical/sql: 0.74.0
  • Adapter: Postgres
  • Node: 24.12.0
  • Call path: @happyvertical/smrt-core DefaultCollection.create() -> SmrtObject.save() -> @happyvertical/sql upsert()

Minimal shape

A direct repro should be close to:

await db.upsert(
  "resume_positions",
  ["slug", "context"],
  { slug: "x", context: "", start: "Dec 2024", end: "Present" },
);

The generated Postgres SQL should quote the end column identifier.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions