Skip to content

feat: enums#110

Closed
jkomyno wants to merge 7 commits into
mainfrom
feat/enums
Closed

feat: enums#110
jkomyno wants to merge 7 commits into
mainfrom
feat/enums

Conversation

@jkomyno
Copy link
Copy Markdown
Contributor

@jkomyno jkomyno commented Jan 12, 2026

draft

This PR:

Summary by CodeRabbit

Release Notes

  • New Features

    • Added native PostgreSQL enum support with predefined role values (USER, ADMIN, MODERATOR)
    • New query function to filter users by role with customizable limits
    • Enhanced schema verification to detect enum inconsistencies and mismatches
  • Tests

    • Comprehensive test coverage for enum creation, validation, and migration scenarios
  • Documentation

    • Added enum handling documentation to PostgreSQL adapter README
    • New architecture decision record on enum persistence strategy
    • Updated testing guide with complete verification workflow

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

Walkthrough

Introduces native enum support across the SQL framework, including enum type definitions in schema IR, Postgres migration planning, contract builder API, adapter codecs, and schema verification logic. Adds example app usage with Role enum and validation tests.

Changes

Cohort / File(s) Summary
Core Schema IR & Type System
packages/2-sql/1-core/schema-ir/src/exports/types.ts, packages/2-sql/1-core/schema-ir/src/types.ts, packages/2-sql/1-core/contract/src/types.ts
Adds SqlEnumIR type with name and values array; extends SqlSchemaIR to include optional enums map. Documentation comments added for enum storage via typeParams.
Contract Builder
packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts, packages/2-sql/2-authoring/contract-ts/test/contract-builder.methods.test.ts
Introduces Enums generic parameter and enum() builder method to define named enums; propagates Enums through all builder methods. Runtime storage construction includes enum type instances when present. Tests verify enum definitions in storage.types.
Enum Extraction & Verification Helpers
packages/2-sql/3-tooling/family/src/core/schema-verify/enum-helpers.ts, packages/2-sql/3-tooling/family/src/exports/schema-verify.ts, packages/2-sql/3-tooling/family/test/schema-verify.helpers.ts
Adds EnumMap type and resolveColumnTypeParams, extractEnumsFromContract functions to extract enums from contract storage and column definitions. Helper test utilities extended to support typeParams in column definitions.
Schema Verification
packages/2-sql/3-tooling/family/src/core/schema-verify/verify-sql-schema.ts, packages/2-sql/3-tooling/family/test/schema-verify.enums.test.ts
Integrates enum verification logic detecting missing enums, value mismatches, and extra enums. Generates enum-specific issue nodes (enum_missing, enum_values_mismatch, extra_enum). Comprehensive test coverage for enum matching scenarios.
Tooling Build Configuration
packages/2-sql/3-tooling/family/.gitignore, packages/2-sql/3-tooling/family/package.json
Adds clean:artifacts script to remove TypeScript declaration files; build script updated to clean artifacts before compilation. .gitignore extended to exclude \.d.ts and \.d.ts.map files.
Postgres Migration Planning
packages/3-targets/3-targets/postgres/src/core/migrations/planner.ts, packages/3-targets/3-targets/postgres/test/migrations/planner.enums.test.ts
Adds enum operation handling (create/append/drop) in migration planner; extracts enums from contract and sequences enum operations before table operations. OperationClass type extended to include 'enum'. Tests validate enum creation, value appending, and drop scenarios.
Postgres Planner Tests
packages/3-targets/3-targets/postgres/test/migrations/planner.behavior.test.ts, packages/3-targets/3-targets/postgres/test/migrations/planner.dependencies.test.ts
Adds test cases for multiple type mismatch sorting and dependency operation ordering. Planner coverage expanded for edge cases.
Postgres Adapter - Codecs & Column Types
packages/3-targets/6-adapters/postgres/src/core/codecs.ts, packages/3-targets/6-adapters/postgres/src/exports/column-types.ts, packages/3-targets/6-adapters/postgres/src/types/codec-types.ts
Introduces pgEnumCodec handling enums as strings; new enumColumn() factory function to create enum column descriptors. CodecTypes type extended with parameterizedOutput for 'pg/enum@1' to derive union types from values.
Postgres Adapter - Type Tests
packages/3-targets/6-adapters/postgres/test/enum-result-type.types.test-d.ts, packages/3-targets/6-adapters/postgres/test/enum-types.test.ts
Type-level tests validate CodecTypes.pg/enum@1 exports output and parameterizedOutput. Runtime tests verify codec registration, encoding/decoding, enumColumn factory, and enum renderer with value escaping and fallback behavior.
Postgres Adapter - Introspection & Metadata
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts, packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts, packages/3-targets/6-adapters/postgres/src/exports/codec-types.ts
Adds database enum introspection query; control adapter populates enums in returned SqlSchemaIR. Descriptor metadata enables nativeEnums capability; registers enum type renderer renderEnumType. CodecTypes export path moved to dedicated types module.
Postgres Adapter Documentation
packages/3-targets/6-adapters/postgres/README.md
Adds nativeEnums capability flag; documents enum identification, migration behavior, and fallback. Expands includeMany lowering strategy details and DML RETURNING examples.
Control Plane
packages/1-framework/1-core/migration/control-plane/src/types.ts
Extends SchemaIssue.kind union with enum_missing, enum_values_mismatch, extra_enum; adds optional enumName field; changes expected/actual types to support string arrays for enum value comparisons.
Example App - Enum Definition
examples/prisma-next-demo/src/enums/role.ts
New module exports ROLE_VALUES constant tuple, Role union type, and DEFAULT_ROLE value ('USER') as single source of truth for role enumeration.
Example App - Contract & Generated Types
examples/prisma-next-demo/prisma/contract.ts, examples/prisma-next-demo/src/prisma/contract.d.ts, examples/prisma-next-demo/src/prisma/contract.json
Defines roleColumn using enumColumn; registers Role enum on contract; adds role column to user table. Generated types include role field with literal type 'USER' | 'ADMIN' | 'MODERATOR' and foreign key mapping.
Example App - Seeding
examples/prisma-next-demo/scripts/seed.ts, examples/prisma-next-demo/test/orm.integration.test.ts, examples/prisma-next-demo/test/runtime.integration.test.ts
Adds id, role, createdAt parameters to user insert payloads; role column included in returning clauses. Tests updated to use DEFAULT_ROLE for seeded users.
Example App - Queries
examples/prisma-next-demo/src/queries/get-users-by-role.ts, examples/prisma-next-demo/src/queries/get-users.ts, examples/prisma-next-demo/src/queries/orm-writes.ts
New getUsersByRole() query filters users by role parameter. getUsers updated to include role in selection. ormCreateUser signature extended to accept optional role parameter with DEFAULT_ROLE fallback.
Example App - Utilities & Error Handling
examples/prisma-next-demo/test/utils/control-client.ts
Adds detailed error logging via console.error for database initialization failures.
Integration Tests
test/integration/test/family.introspect.integration.test.ts
Adds timeout parameter (timeouts.spinUpPgDev) to connection error test cases.
Documentation
docs/Testing Guide.md, docs/architecture docs/adrs/ADR 155 - Enum Persistence Strategy.md
Full Verification Workflow section added detailing complete test/build sequence. ADR 155 documents unified enum persistence approach via parameterized storage.types entries, codec ownership, contract builder API, and extraction strategy.
Postgres Planner Coverage
packages/3-targets/3-targets/postgres/vitest.config.ts
Excludes descriptor-meta.ts from coverage reporting.

Sequence Diagrams

sequenceDiagram
    participant User
    participant ContractBuilder
    participant SchemaIR
    participant PostgresAdapter
    participant MigrationPlanner
    participant Database

    User->>ContractBuilder: define enum('Role', ['USER','ADMIN'])
    ContractBuilder->>SchemaIR: store as SqlEnumIR in types
    Note over SchemaIR: SqlSchemaIR.enums { Role: { values: [...] } }

    User->>PostgresAdapter: build contract
    PostgresAdapter->>PostgresAdapter: generate pgEnumCodec
    PostgresAdapter->>PostgresAdapter: register enumColumn descriptor
    Note over PostgresAdapter: Codec maps string↔string with nativeType

    MigrationPlanner->>SchemaIR: extract enums from contract
    MigrationPlanner->>Database: verify enum against schema
    alt Enum missing
        MigrationPlanner->>Database: CREATE TYPE "Role" AS ENUM (...)
    else Enum values mismatch (append-only)
        MigrationPlanner->>Database: ALTER TYPE "Role" ADD VALUE 'NEWVAL'
    else Enum extra (strict mode)
        MigrationPlanner->>Database: DROP TYPE "Role"
    end
    Database->>User: migrations applied
Loading
sequenceDiagram
    participant SchemaVerifier
    participant ContractStorage
    participant DatabaseSchema
    participant VerificationResult

    SchemaVerifier->>ContractStorage: extractEnumsFromContract()
    Note over ContractStorage: Prioritize storage.types<br/>Fall back to column typeParams
    ContractStorage->>SchemaVerifier: EnumMap { Role: ['USER','ADMIN','MODERATOR'] }

    SchemaVerifier->>DatabaseSchema: introspect enums
    DatabaseSchema->>SchemaVerifier: enums { Role: ['USER','ADMIN'] }

    SchemaVerifier->>SchemaVerifier: compare values
    alt Match
        SchemaVerifier->>VerificationResult: status: pass
    else Append-only mismatch
        SchemaVerifier->>VerificationResult: issue: enum_values_mismatch<br/>expected: ['USER','ADMIN','MODERATOR']<br/>actual: ['USER','ADMIN']
    else Not append-only (strict)
        SchemaVerifier->>VerificationResult: issue: typeMismatch
    else Missing in database
        SchemaVerifier->>VerificationResult: issue: enum_missing
    else Extra in database (strict)
        SchemaVerifier->>VerificationResult: issue: extra_enum
    end
Loading


📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Free

📥 Commits

Reviewing files that changed from the base of the PR and between c4ece41 and e7f1386.

📒 Files selected for processing (14)
  • docs/architecture docs/adrs/ADR 155 - Enum Persistence Strategy.md
  • examples/prisma-next-demo/prisma/contract.ts
  • examples/prisma-next-demo/src/enums/role.ts
  • examples/prisma-next-demo/src/queries/get-users-by-role.ts
  • examples/prisma-next-demo/src/queries/orm-writes.ts
  • examples/prisma-next-demo/test/orm.integration.test.ts
  • examples/prisma-next-demo/test/runtime.integration.test.ts
  • packages/2-sql/1-core/contract/src/types.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.methods.test.ts
  • packages/2-sql/3-tooling/family/src/core/schema-verify/enum-helpers.ts
  • packages/2-sql/3-tooling/family/test/schema-verify.enums.test.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/planner.ts
  • packages/3-targets/3-targets/postgres/test/migrations/planner.enums.test.ts

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented Jan 12, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Base automatically changed from tml-1808-phase-6-pgvector-types to main January 12, 2026 10:00
@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented Jan 12, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jkomyno jkomyno marked this pull request as ready for review January 13, 2026 07:08
@jkomyno jkomyno requested a review from wmadden January 13, 2026 07:09
@jkomyno

This comment was marked as outdated.

This comment was marked as off-topic.

* const mods = await getUsersByRole('MODERATOR', 5);
* ```
*/
export async function getUsersByRole(role: 'USER' | 'ADMIN' | 'MODERATOR', limit = 10) {
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.

typeof role should be defined as its own type, centralized somewhere.
This should be exported from ../prisma/query.

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.

Created src/enums/role.ts with ROLE_VALUES, Role type, DEFAULT_ROLE

const plan = orm.user().create({
id: data.id,
email: data.email,
role: data.role ?? 'USER',
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.

The default value for role should come from a centralized module.

})
.returning(userTable.columns['id']!)
.build({ params: { id, email, createdAt } });
.build({ params: { id, email, role: 'USER', createdAt } });
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.

})
.returning(userTable.columns['id']!)
.build({ params: { id, email, createdAt } });
.build({ params: { id, email, role: 'USER', createdAt } });
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.

CoreHash extends string | undefined = string | undefined,
ExtensionPacks extends Record<string, unknown> | undefined = undefined,
Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
Enums extends Record<string, readonly string[]> = Record<never, never>,
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.

In a future iteration, we should allow users to specify whether they want string or number as enum keys.

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.

The contract should define how enums are persisted.

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.

Documented this in ADR 155

Comment on lines +185 to +201
const pgEnumCodec = codec<'pg/enum@1', string, string>({
typeId: 'pg/enum@1',
targetTypes: [], // Enum types are dynamically determined by nativeType
encode: (value) => value,
decode: (wire) => wire,
meta: {
db: {
sql: {
postgres: {
// The actual enum type name is specified per-column via nativeType.
// This placeholder indicates it's an enum without specifying which one.
nativeType: 'enum',
},
},
},
},
});
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.

We will want to add new codecs that may be backed by number, not string. That will require an explicit mapping in the contract.


interface PlannerConfig {
readonly defaultSchema: string;
readonly supportsNativeEnums: boolean;
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.

This should be a capability, not a boolean.

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.

However, we're in a Postgres-specific migration planner. This boolean will always be true, so let's drop it.

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.

Removed this from the planner, kept track of it in ADR 155

operations.push({
id: `enum.${tableName}.${columnName}.check`,
label: `Add enum check on ${tableName}.${columnName}`,
summary: `Adds CHECK constraint for enum ${column.nativeType} on ${tableName}.${columnName}`,
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.

Keep this idea around for new databases that:

  • support CHECK constraints
  • do not support native enums

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.

Kept track of it in ADR 155

Comment on lines +96 to +101
readonly types?: Record<string, StorageTypeInstance>;
/**
* Enum type definitions.
* Enums can be referenced by columns via their nativeType matching the enum name.
*/
readonly enums?: Record<string, EnumDefinition>;
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.

Why do we need both of these? The whole idea with paramaterized types was to then build enums as a parameterized type, not define a specialized abstraction for them

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.

Agreed, fixed.
Enums now live in storage.types as regular StorageTypeInstance entries:

- storage.enums.Role = { values: [...] }
+ storage.types.Role = { codecId: 'pg/enum@1', nativeType: 'Role', typeParams: { values: [...] } }

I also described this in ADR 155.

CoreHash extends string | undefined = undefined,
ExtensionPacks extends Record<string, unknown> | undefined = undefined,
Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
Enums extends Record<string, readonly string[]> = Record<never, never>,
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.

I'm a bit concerned seeing Enums in the framework layer. Are they a necessary abstraction at this layer, because this couples all database families, not just targets, to the enum abstraction

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.

Right. The Framework layer is now enum-agnostic.
The .enum() method only exists on SqlContractBuilder to add entries to state.types under the hood. There's no coupling with Framework

[name]: values,
} as Enums & Record<Name, Values>,
};
// Type assertion needed because base ContractBuilder doesn't know about Enums generic
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.

So the base ContractBuilder remains unaware of enums but the base Contract type gained the Enums type parameter?

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.

I fixed the inconsistency. Both are now unaware:

  • ContractBuilder: no Enums generic
  • ContractBuilderState: no enums field
  • SqlContract:no storage.enums field

SqlContractBuilder.enum('Role', [...]) writes to this.state.types, which already exists. The Contract type remains clean.

Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

High-level

The current implementation adds Postgres enum support, but it does so by breaking the “parameterized type params are opaque/codec-owned” contract and by coupling common SQL logic to enum semantics (and sometimes to a specific codec id). That creates a long-term maintenance hazard and blocks the “strategy/fallback per target (native enum vs CHECK constraint vs other)” goal.

Primary concerns (architectural)

1) extractEnumsFromContract() violates encapsulation

Today it treats any parameterized type with typeParams.values: string[] as an enum (duck typing). That directly contradicts the intended design that typeParams is a black box owned by the codec.

  • File: packages/2-sql/3-tooling/family/src/core/schema-verify/enum-helpers.ts
  • Impact:
    • Any future parameterized codec that uses a values field (common) could be incorrectly treated as an enum.
    • Common SQL-family logic is now coupled to a specific param shape, not codec identity/behavior.

2) Common SQL logic is coupled to specific codec ids

Even beyond extraction, there is explicit hardcoding:

  • Postgres planner has enum-specific DDL and special-cases pg/enum@1 for column type rendering.
    • File: packages/3-targets/3-targets/postgres/src/core/migrations/planner.ts (renderColumnType checks column.codecId === 'pg/enum@1')
  • TS contract builder hardcodes enum definitions to pg/enum@1, making other strategies/targets impossible without rewriting authoring.
    • File: packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts (BuildEnumTypeInstance uses codecId: 'pg/enum@1')

This is not compatible with the goal of “planner/assembly/common SQL logic must not be coupled to a specific codec id”.

3) ADR 155 valueType is the wrong direction

ADR 155 suggests adding valueType into typeParams to support number-backed enums. This further incentivizes “shared logic interpreting codec params” and deepens the abstraction violation. If params are codec-owned, “number-backed enums” should be represented by a different codec id/version, not a shared valueType convention.

  • File: docs/architecture docs/adrs/ADR 155 - Enum Persistence Strategy.md

4) Schema IR and SchemaIssue encode enums as first-class concepts

Even if enums are implemented via parameterized types, the control plane now has:

  • SqlSchemaIR.enums (enum-specific)
  • SchemaIssue includes enum_missing, enum_values_mismatch, extra_enum (enum-specific)

This pushes enum semantics into shared/common layers, contrary to “framework unaware of enums”.

Behavioral issues / mismatches to fix

  • db init policy conflict: the Postgres planner can emit enum.*.drop as destructive, but INIT_ADDITIVE_POLICY only allows additive. There’s a test expecting drop planning even under init policy; that’s inconsistent with “init is additive-only”.

    • Files: packages/2-sql/3-tooling/family/src/core/migrations/policies.ts, packages/3-targets/3-targets/postgres/test/migrations/planner.enums.test.ts
  • Schema tree output omits enums: SqlFamilyInstance.toSchemaView() renders tables + extensions but not enums, so introspection output isn’t visible in the CLI tree.

    • File: packages/2-sql/3-tooling/family/src/core/instance.ts

Recommended direction (general solution)

Implement codec-provided control-plane hooks (DDL + verify + introspect semantics) keyed by codec identity, analogous to how parameterized type rendering is already assembled today (types.codecTypes.parameterized via extractParameterizedRenderers()).

  • Goal: common SQL logic should:
    • group storage.types by codecId
    • dispatch to codec-owned control hooks
    • never inspect typeParams structure (no values, no valueType, no duck typing)
    • never special-case codec ids in planners/verifiers

This also enables multiple enum strategies (native enum type vs check constraint) as different codecs, without teaching the core/family about enums.

@linear
Copy link
Copy Markdown

linear Bot commented Jan 15, 2026

TML-1818 [PN] Introduce enums

This ticket only concerns with the prisma-next implementation, whereas TML-1389 will be about using such implementation in pdp.

@jkomyno jkomyno closed this Jan 28, 2026
@jkomyno jkomyno mentioned this pull request Jan 28, 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.

3 participants