Skip to content

fix(sdk): keep .unique() on a field with a keyOnly relation - #2919

Merged
dqn merged 3 commits into
mainfrom
fix/keyonly-relation-keeps-unique
Nov 3, 2026
Merged

dqn merged 3 commits into
mainfrom
fix/keyonly-relation-keeps-unique

Conversation

@dqn

@dqn dqn commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

.unique() on a field with a keyOnly relation was silently dropped, so the field was deployed without a unique constraint. The field now keeps it.

What changes

  • db.uuid().unique().relation({ type: "keyOnly", ... }) used to deploy the field without a unique constraint; the constraint is now applied, and the PGlite and seed schemas declare it too.
  • A relation whose toward.key points at such a field used to fail with references "User.accountID", which must be unique.; it is now accepted.
  • A keyOnly field without .unique() stays non-unique.

Notes

  • A deployed table that combines keyOnly and .unique() gains the unique constraint on its next deploy. The changeset describes the steps, including MIGRATION_SCHEMA_CHECK_FAILED and the generated migrate.ts in a namespace with migration.directory.
  • Rejecting keyOnly + .unique() as n-1 does was considered, but existing projects would then fail to load, and a foreign key on a unique field is consistent, so the field keeps .unique().
  • An array field with a relation still drops .unique() (reachable through .clone({ array: true })); this is left for a separate fix.

Verification

On disposable workspaces, deployed with main (0c17d81), inserted two Member rows with the same accountID, then deployed this branch (287925a):

  • Without migration.directory: tailor deploy failed with could not create unique index "u_idx_Member_attribute_accountID" (SQLSTATE 23505). After deleting one duplicate it succeeded, and creating a duplicate then failed with key already exists.
  • With migration.directory: tailor deploy stopped with MIGRATION_SCHEMA_CHECK_FAILED (accountID: unique: false → true). tailor tailordb migration generate --yes reported Unique constraint added to field and generated migrate.ts, and deploy then stopped with MIGRATION_SCRIPT_REVIEW_REQUIRED. With TODO() replaced by a UUID not in Account, the migration failed with violates foreign key constraint; with an unused Account id, the deploy succeeded and a new duplicate failed with key already exists.

The relation metadata overwrote the field's own unique with the value
the relation type implies, so keyOnly always dropped an explicit
.unique(). The field was deployed without a unique constraint, and a
relation using it as toward.key was rejected as non-unique.

processRelationMetadata now takes the field's modifiers and keeps an
explicit unique; the toward.key check derives uniqueness the same way.
Array fields keep their current behavior.
@changeset-bot

changeset-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 80f0150

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@tailor-platform/sdk Patch
@tailor-platform/create-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@tailor-platform/create-sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/create-sdk@80f0150

@tailor-platform/eslint-plugin-sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/eslint-plugin-sdk@80f0150

@tailor-platform/sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk@80f0150

@tailor-platform/sdk-plugin-frontend

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-frontend@80f0150

@tailor-platform/sdk-plugin-seed

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-seed@80f0150

@tailor-platform/sdk-plugin-setup

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-setup@80f0150

@tailor-platform/sdk-plugin-tailordb-erd

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-tailordb-erd@80f0150

commit: 80f0150

@github-actions

This comment has been minimized.

@dqn
dqn marked this pull request as ready for review November 3, 2026 00:46
@dqn
dqn requested a review from a team as a code owner November 3, 2026 00:46
@dqn
dqn requested a review from toiroakr November 3, 2026 00:46

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📖 Docs Quality & Consistency Check

✅ Recommended Actions

None — docs are consistent with the implementation and contain no user-facing internal-detail leaks.

⚠️ Consistency Issues

No consistency issues found.

📝 Reader-Perspective Issues

No reader-perspective issues found.

🔍 Details

Areas Checked

User-Facing Documentation:

  • packages/sdk/docs/services/tailordb/index.md (lines 218-231, 268-271)
  • .changeset/wet-days-throw.md

Implementation Files:

  • packages/sdk/src/parser/service/tailordb/relation.ts — processRelationMetadata signature change and logic
  • packages/sdk/src/parser/service/tailordb/type-parser.ts — relation metadata application
  • packages/sdk/src/parser/service/tailordb/type-parser.test.ts — test coverage for the new behavior

Consistency Verification

Documentation Claims vs. Implementation:

  1. keyOnly relations and uniqueness (docs line 229):

    • Docs: "A keyOnly relation does not make the field unique; add .unique() to the field to make it unique."
    • Implementation: isUnique = relationTypes[rawRelation.type] === "1-1" || field.unique === true — keyOnly is not "1-1", so it only becomes unique if field.unique === true ✅
  2. Nested relations in db.object() (docs line 231):

    • Docs: "Inside a db.object() that is not an array, .unique() and .index() on the inner field work as described in Index / Unique."
    • Implementation: processRelationMetadata now receives field parameter with both array and unique properties, and nested relation processing in applyNestedRelationMetadata preserves these modifiers ✅
    • Tests: "keeps .unique() and .index() set on the inner field" test verifies this behavior ✅
  3. toward.key uniqueness requirement (docs line 270):

    • Docs: "A custom toward.key must be unique, through .unique() or a one-to-one relation."
    • Implementation: validateRelationConfig checks keyIsUnique by examining either the key field's relation metadata or its own .unique() modifier ✅
    • Tests: "accepts a relation to a key made unique by %s" test verifies that fields with .unique() on a keyOnly relation can be used as toward.key ✅
  4. Array fields (docs line 178, changeset):

    • Docs: "Inside an array of objects (db.object(..., { array: true })) the platform does not reject duplicate values, so both [.unique() and .index()] are rejected there with an error"
    • Implementation: shouldSetUnique = !field.array && isUnique — unique is not set on array fields ✅
    • Changeset: "An array field with a relation still drops .unique() (reachable through .clone({ array: true })); this is left for a separate fix." — accurately describes current behavior ✅
  5. Migration behavior (changeset):

    • Changeset describes upgrade path with MIGRATION_SCHEMA_CHECK_FAILED, tailor tailordb migration generate, and migrate.ts with TODO() placeholders
    • This matches the documented migration workflow pattern in the "Index / Unique" section (line 182-184) ✅

Reader-Perspective Review

User-Facing Documentation Checked:

  • packages/sdk/docs/services/tailordb/index.md — No internal implementation details leaked; describes only user-observable behavior (relation types, modifiers, constraints)
  • .changeset/wet-days-throw.md — Mentions kyselyTypePlugin, seedPlugin, db.pglite.ts, migrate.ts, MIGRATION_SCHEMA_CHECK_FAILED, and TODO(), all of which are user-facing APIs, files, and error codes that users interact with

Internal Code Changes:

  • JSDoc changes in packages/sdk/src/parser/service/tailordb/relation.ts were reviewed but are not user-facing (parser module is internal, not re-exported through package.json#exports)

Example Code:

  • example/tailordb/salesOrder.ts uses keyOnly on an array field, which is unaffected by this PR

Re-run this check by adding the docs-check label to the PR.


inner.rawRelation,
innerContext,
inner.array,
inner,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change does not alter behavior. applyNestedRelationMetadata only reads foreignKey, foreignKeyType and foreignKeyField from the result and spreads inner, so the inner unique and index are kept either way.
Reverting this call to { array: inner.array } leaves all 82 tests in type-parser.test.ts passing, including the new nested test, so that test cannot catch a regression.

Please drop this call change and the nested test, and reduce the docs paragraph (index.md:231) to one sentence: a relation adds only the foreign key to an inner field, and .unique() / .index() set on it work as in Index / Unique.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4516280.

expect(result.Profile!.fields.userId!.config.unique).toBe(true);
});

test.each([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, can be a separate PR: this test only checks config.unique, and nothing checks that the PGlite DDL gets UNIQUE.

Please add a keyOnly field with .unique() to the "relations written in the table definition" block in pglite-schema.test.ts and assert that the DDL contains UNIQUE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 80f0150.

@toiroakr toiroakr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall. See inline comments.

@toiroakr toiroakr assigned dqn and unassigned toiroakr Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Code Metrics Report (packages/sdk)

Complexity change of this pull request: cognitive +1, cyclomatic +1
Function File Cognitive Cyclomatic
processRelationMetadata packages/sdk/src/parser/service/tailordb/relation.ts:127 1 → 2 (+1) 2 → 3 (+1)

Compared with the base commit of this pull request.

main (04dc1c7) #2919 (cbfc8a5) +/-
Coverage 89.2% 89.2% 0.0%
Code to Test Ratio 1:0.5 1:0.5 +0.0
Details
  |                    | main (04dc1c7) | #2919 (cbfc8a5) | +/-  |
  |--------------------|----------------|-----------------|------|
  | Coverage           |          89.2% |           89.2% | 0.0% |
  |   Files            |            578 |             578 |    0 |
  |   Lines            |          25815 |           25815 |    0 |
  |   Covered          |          23035 |           23035 |    0 |
+ | Code to Test Ratio |          1:0.5 |           1:0.5 | +0.0 |
  |   Code             |         212577 |          212602 |  +25 |
+ |   Test             |         120407 |          120432 |  +25 |

Code coverage of files in pull request scope (90.3% → 90.3%, patch 100.0%)

Files Coverage +/- Patch Coverage Status
packages/sdk/src/parser/service/tailordb/relation.ts 95.2% 0.0% 100.0% modified
packages/sdk/src/parser/service/tailordb/type-parser.ts 88.8% 0.0% 100.0% modified

SDK Configure Bundle Size

main (04dc1c7) #2919 (cbfc8a5) +/-
configure-index-size 13.82KB 13.82KB 0KB
dependency-chunks-size 25.76KB 25.76KB 0KB
total-bundle-size 39.58KB 39.58KB 0KB

Runtime Performance

main (04dc1c7) #2919 (cbfc8a5) +/-
Generate Median 1,513ms 1,795ms +282ms
Generate Max 2,259ms 2,273ms +14ms
Apply Build Median 1,617ms 1,947ms +330ms
Apply Build Max 1,676ms 2,852ms +1,176ms

Type Performance (instantiations)

main (04dc1c7) #2919 (cbfc8a5) +/-
tailordb-baseline 955 955 0
resolver-baseline 4,641 4,641 0
executor-baseline 848 848 0
tailordb-basic 2,385 2,385 0
tailordb-optional 1,167 1,167 0
tailordb-relation 3,808 3,808 0
tailordb-validate 186 186 0
tailordb-hooks 999 999 0
tailordb-object 1,892 1,892 0
tailordb-enum 574 574 0
resolver-basic 1,022 1,022 0
resolver-nested 3,405 3,405 0
resolver-array 1,954 1,954 0
executor-schedule 1,388 1,388 0
executor-webhook 47 47 0
executor-record-created 3,219 3,219 0
executor-record-updated 3,219 3,219 0
executor-record-deleted 3,219 3,219 0
executor-resolver 463 463 0
executor-operation-function 30 30 0
executor-operation-gql 43 43 0
executor-operation-webhook 50 50 0
executor-operation-workflow 211 211 0

Reported by octocov

@dqn
dqn merged commit d8a350e into main Nov 3, 2026
57 checks passed
@dqn
dqn deleted the fix/keyonly-relation-keeps-unique branch November 3, 2026 08:35
@github-actions github-actions Bot mentioned this pull request Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants