@prisma-next/adapter-postgres 0.15.0 → 0.16.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,8 @@
1
- import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
1
+ import type {
2
+ ColumnDefault,
3
+ ContractMarkerRecord,
4
+ LedgerEntryRecord,
5
+ } from '@prisma-next/contract/types';
2
6
  import {
3
7
  parseMarkerRowSafely,
4
8
  rethrowMarkerReadError,
@@ -7,7 +11,7 @@ import {
7
11
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
8
12
  import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
9
13
  import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
10
- import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
14
+ import { APP_SPACE_ID, type SchemaNodeRef } from '@prisma-next/framework-components/control';
11
15
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
12
16
  import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
13
17
  import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
@@ -36,6 +40,7 @@ import type {
36
40
  SqlReferentialAction,
37
41
  SqlUniqueIRInput,
38
42
  } from '@prisma-next/sql-schema-ir/types';
43
+ import { RelationalSchemaNodeKind } from '@prisma-next/sql-schema-ir/types';
39
44
  import {
40
45
  buildControlTableBootstrapQueries,
41
46
  buildSignMarkerBootstrapQueries,
@@ -66,6 +71,7 @@ import {
66
71
  PostgresNativeEnumSchemaNode,
67
72
  PostgresPolicySchemaNode,
68
73
  PostgresRoleSchemaNode,
74
+ PostgresSchemaNodeKind,
69
75
  PostgresTableSchemaNode,
70
76
  } from '@prisma-next/target-postgres/types';
71
77
  import { blindCast } from '@prisma-next/utils/casts';
@@ -710,6 +716,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
710
716
  numeric_scale: number | null;
711
717
  column_default: string | null;
712
718
  formatted_type: string | null;
719
+ attidentity: string;
713
720
  }>(
714
721
  `SELECT
715
722
  c.table_name,
@@ -721,7 +728,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
721
728
  numeric_precision,
722
729
  numeric_scale,
723
730
  column_default,
724
- format_type(a.atttypid, a.atttypmod) AS formatted_type
731
+ format_type(a.atttypid, a.atttypmod) AS formatted_type,
732
+ a.attidentity
725
733
  FROM information_schema.columns c
726
734
  JOIN pg_catalog.pg_class cl
727
735
  ON cl.relname = c.table_name
@@ -737,7 +745,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
737
745
  ORDER BY c.table_name, c.ordinal_position`,
738
746
  [schema],
739
747
  );
740
- // Query all primary keys for all tables in schema
748
+ // Query all primary keys for all tables in schema. Reads pg_catalog, not
749
+ // information_schema: `information_schema.table_constraints` hides
750
+ // constraints on tables where the connecting role's only privilege is
751
+ // SELECT (its filter grants visibility for INSERT/UPDATE/DELETE/TRUNCATE/
752
+ // REFERENCES/TRIGGER — not SELECT), so a SELECT-only table introspects
753
+ // with all its columns but zero constraints and verify reports every
754
+ // declared constraint as missing. pg_catalog is not privilege-filtered.
741
755
  const pkResult = await driver.query<{
742
756
  table_name: string;
743
757
  constraint_name: string;
@@ -745,24 +759,29 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
745
759
  ordinal_position: number;
746
760
  }>(
747
761
  `SELECT
748
- tc.table_name,
749
- tc.constraint_name,
750
- kcu.column_name,
751
- kcu.ordinal_position
752
- FROM information_schema.table_constraints tc
753
- JOIN information_schema.key_column_usage kcu
754
- ON tc.constraint_name = kcu.constraint_name
755
- AND tc.table_schema = kcu.table_schema
756
- AND tc.table_name = kcu.table_name
757
- WHERE tc.table_schema = $1
758
- AND tc.constraint_type = 'PRIMARY KEY'
759
- ORDER BY tc.table_name, kcu.ordinal_position`,
762
+ cl.relname AS table_name,
763
+ con.conname AS constraint_name,
764
+ a.attname AS column_name,
765
+ k.ord AS ordinal_position
766
+ FROM pg_catalog.pg_constraint con
767
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
768
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
769
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
770
+ JOIN pg_catalog.pg_attribute a
771
+ ON a.attrelid = con.conrelid
772
+ AND a.attnum = k.attnum
773
+ WHERE ns.nspname = $1
774
+ AND con.contype = 'p'
775
+ ORDER BY cl.relname, k.ord`,
760
776
  [schema],
761
777
  );
762
- // Query all foreign keys for all tables in schema, including referential actions.
763
- // Uses pg_catalog for correct positional pairing of composite FK columns
764
- // (information_schema.constraint_column_usage lacks ordinal_position,
765
- // which causes Cartesian products for multi-column FKs).
778
+ // Query all foreign keys for all tables in schema, including referential
779
+ // actions. Reads pg_catalog only (see the primary-key query above for why
780
+ // information_schema is unusable here); `unnest(conkey) WITH ORDINALITY`
781
+ // pairs source and referenced columns positionally, so composite FKs
782
+ // round-trip in declared order. The referential-action CASEs mirror
783
+ // pg_constraint's confdeltype/confupdtype encoding and produce the same
784
+ // rule strings information_schema.referential_constraints reports.
766
785
  const fkResult = await driver.query<{
767
786
  table_name: string;
768
787
  constraint_name: string;
@@ -775,41 +794,48 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
775
794
  update_rule: string;
776
795
  }>(
777
796
  `SELECT
778
- tc.table_name,
779
- tc.constraint_name,
780
- kcu.column_name,
781
- kcu.ordinal_position,
797
+ cl.relname AS table_name,
798
+ con.conname AS constraint_name,
799
+ a.attname AS column_name,
800
+ k.ord AS ordinal_position,
782
801
  ref_ns.nspname AS referenced_table_schema,
783
802
  ref_cl.relname AS referenced_table_name,
784
803
  ref_att.attname AS referenced_column_name,
785
- rc.delete_rule,
786
- rc.update_rule
787
- FROM information_schema.table_constraints tc
788
- JOIN information_schema.key_column_usage kcu
789
- ON tc.constraint_name = kcu.constraint_name
790
- AND tc.table_schema = kcu.table_schema
791
- AND tc.table_name = kcu.table_name
792
- JOIN pg_catalog.pg_constraint pgc
793
- ON pgc.conname = tc.constraint_name
794
- AND pgc.connamespace = (
795
- SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
796
- )
804
+ CASE con.confdeltype
805
+ WHEN 'a' THEN 'NO ACTION'
806
+ WHEN 'r' THEN 'RESTRICT'
807
+ WHEN 'c' THEN 'CASCADE'
808
+ WHEN 'n' THEN 'SET NULL'
809
+ WHEN 'd' THEN 'SET DEFAULT'
810
+ END AS delete_rule,
811
+ CASE con.confupdtype
812
+ WHEN 'a' THEN 'NO ACTION'
813
+ WHEN 'r' THEN 'RESTRICT'
814
+ WHEN 'c' THEN 'CASCADE'
815
+ WHEN 'n' THEN 'SET NULL'
816
+ WHEN 'd' THEN 'SET DEFAULT'
817
+ END AS update_rule
818
+ FROM pg_catalog.pg_constraint con
819
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
820
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
821
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
822
+ JOIN pg_catalog.pg_attribute a
823
+ ON a.attrelid = con.conrelid
824
+ AND a.attnum = k.attnum
797
825
  JOIN pg_catalog.pg_class ref_cl
798
- ON ref_cl.oid = pgc.confrelid
826
+ ON ref_cl.oid = con.confrelid
799
827
  JOIN pg_catalog.pg_namespace ref_ns
800
828
  ON ref_ns.oid = ref_cl.relnamespace
801
829
  JOIN pg_catalog.pg_attribute ref_att
802
- ON ref_att.attrelid = pgc.confrelid
803
- AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
804
- JOIN information_schema.referential_constraints rc
805
- ON rc.constraint_name = tc.constraint_name
806
- AND rc.constraint_schema = tc.table_schema
807
- WHERE tc.table_schema = $1
808
- AND tc.constraint_type = 'FOREIGN KEY'
809
- ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
830
+ ON ref_att.attrelid = con.confrelid
831
+ AND ref_att.attnum = con.confkey[k.ord]
832
+ WHERE ns.nspname = $1
833
+ AND con.contype = 'f'
834
+ ORDER BY cl.relname, con.conname, k.ord`,
810
835
  [schema],
811
836
  );
812
- // Query all unique constraints for all tables in schema (excluding PKs)
837
+ // Query all unique constraints for all tables in schema (excluding PKs).
838
+ // Reads pg_catalog (see the primary-key query above).
813
839
  const uniqueResult = await driver.query<{
814
840
  table_name: string;
815
841
  constraint_name: string;
@@ -817,18 +843,20 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
817
843
  ordinal_position: number;
818
844
  }>(
819
845
  `SELECT
820
- tc.table_name,
821
- tc.constraint_name,
822
- kcu.column_name,
823
- kcu.ordinal_position
824
- FROM information_schema.table_constraints tc
825
- JOIN information_schema.key_column_usage kcu
826
- ON tc.constraint_name = kcu.constraint_name
827
- AND tc.table_schema = kcu.table_schema
828
- AND tc.table_name = kcu.table_name
829
- WHERE tc.table_schema = $1
830
- AND tc.constraint_type = 'UNIQUE'
831
- ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
846
+ cl.relname AS table_name,
847
+ con.conname AS constraint_name,
848
+ a.attname AS column_name,
849
+ k.ord AS ordinal_position
850
+ FROM pg_catalog.pg_constraint con
851
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
852
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
853
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
854
+ JOIN pg_catalog.pg_attribute a
855
+ ON a.attrelid = con.conrelid
856
+ AND a.attnum = k.attnum
857
+ WHERE ns.nspname = $1
858
+ AND con.contype = 'u'
859
+ ORDER BY cl.relname, con.conname, k.ord`,
832
860
  [schema],
833
861
  );
834
862
  // Query all indexes for all tables in schema (excluding constraints).
@@ -873,10 +901,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
873
901
  WHERE i.schemaname = $1
874
902
  AND NOT EXISTS (
875
903
  SELECT 1
876
- FROM information_schema.table_constraints tc
877
- WHERE tc.table_schema = $1
878
- AND tc.table_name = i.tablename
879
- AND tc.constraint_name = i.indexname
904
+ FROM pg_catalog.pg_constraint con
905
+ WHERE con.conindid = ic.oid
906
+ AND con.contype IN ('p', 'u', 'x')
880
907
  )
881
908
  ORDER BY i.tablename, i.indexname, k.ord`,
882
909
  [schema],
@@ -986,6 +1013,21 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
986
1013
  // normalizes them itself.
987
1014
  const resolvedNativeType = `${normalizeSchemaNativeType(nativeType)}${many ? '[]' : ''}`;
988
1015
  const rawDefault = colRow.column_default ?? undefined;
1016
+ // `GENERATED ALWAYS AS IDENTITY` ('a') and `GENERATED BY DEFAULT AS
1017
+ // IDENTITY` ('d') both report a NULL column_default — Postgres tracks
1018
+ // generation via attidentity, not a default expression — so neither
1019
+ // variant is visible in `rawDefault` at all. The contract has no
1020
+ // syntax to distinguish the two, so both resolve directly to the same
1021
+ // `autoincrement()` a `serial` column's `nextval(...)` default
1022
+ // already maps to. This is the only place identity is recognized —
1023
+ // there is no `SqlColumnIR.identity` field; every consumer compares
1024
+ // `resolvedDefault` instead.
1025
+ const isIdentityColumn = colRow.attidentity === 'a' || colRow.attidentity === 'd';
1026
+ const resolvedDefault: ColumnDefault | undefined = isIdentityColumn
1027
+ ? { kind: 'function', expression: 'autoincrement()' }
1028
+ : rawDefault !== undefined
1029
+ ? parsePostgresDefault(rawDefault, resolvedNativeType)
1030
+ : undefined;
989
1031
  columns[colRow.column_name] = {
990
1032
  name: colRow.column_name,
991
1033
  nativeType,
@@ -993,12 +1035,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
993
1035
  ...ifDefined('default', rawDefault),
994
1036
  ...ifDefined('many', many),
995
1037
  resolvedNativeType,
996
- ...ifDefined(
997
- 'resolvedDefault',
998
- rawDefault !== undefined
999
- ? parsePostgresDefault(rawDefault, resolvedNativeType)
1000
- : undefined,
1001
- ),
1038
+ ...ifDefined('resolvedDefault', resolvedDefault),
1002
1039
  };
1003
1040
  }
1004
1041
 
@@ -1012,6 +1049,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1012
1049
  ? {
1013
1050
  columns: primaryKeyColumns,
1014
1051
  ...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
1052
+ dependsOn: postgresColumnDependsOn(schema, tableName, primaryKeyColumns),
1015
1053
  }
1016
1054
  : undefined;
1017
1055
 
@@ -1054,6 +1092,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1054
1092
  name: fk.name,
1055
1093
  ...ifDefined('onDelete', mapReferentialAction(fk.deleteRule)),
1056
1094
  ...ifDefined('onUpdate', mapReferentialAction(fk.updateRule)),
1095
+ dependsOn: [
1096
+ postgresTableDependsOn(fk.referencedSchema, fk.referencedTable),
1097
+ ...postgresColumnDependsOn(schema, tableName, fk.columns),
1098
+ ],
1057
1099
  }),
1058
1100
  );
1059
1101
 
@@ -1078,6 +1120,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1078
1120
  const uniques: readonly SqlUniqueIRInput[] = Array.from(uniquesMap.values()).map((uq) => ({
1079
1121
  columns: Object.freeze([...uq.columns]) as readonly string[],
1080
1122
  name: uq.name,
1123
+ dependsOn: postgresColumnDependsOn(schema, tableName, uq.columns),
1081
1124
  }));
1082
1125
 
1083
1126
  // Process indexes
@@ -1159,6 +1202,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1159
1202
  unique: idx.unique,
1160
1203
  ...(idx.type !== undefined && { type: idx.type }),
1161
1204
  ...(idx.options !== undefined && { options: idx.options }),
1205
+ dependsOn: postgresColumnDependsOn(schema, tableName, idx.columns),
1162
1206
  }),
1163
1207
  );
1164
1208
 
@@ -1241,6 +1285,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1241
1285
  ...(row.qual !== null ? { using: row.qual } : {}),
1242
1286
  ...(row.with_check !== null ? { withCheck: row.with_check } : {}),
1243
1287
  permissive,
1288
+ dependsOn: [
1289
+ postgresTableDependsOn(row.schemaname, row.tablename),
1290
+ ...policyRoles.map(postgresRoleDependsOn),
1291
+ ],
1244
1292
  });
1245
1293
  const list = policiesByTable.get(row.tablename) ?? [];
1246
1294
  list.push(policy);
@@ -1502,6 +1550,44 @@ function mapReferentialAction(rule: string): SqlReferentialAction | undefined {
1502
1550
  return mapped;
1503
1551
  }
1504
1552
 
1553
+ /**
1554
+ * A FK/policy dependency chain, mirroring the shape the expected-side
1555
+ * derivation stamps (`contractToPostgresDatabaseSchemaNode`): the database
1556
+ * root's fixed sentinel id, then the namespace, then the table.
1557
+ */
1558
+ function postgresTableDependsOn(namespaceId: string, tableName: string): SchemaNodeRef {
1559
+ return [
1560
+ { nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
1561
+ { nodeKind: PostgresSchemaNodeKind.namespace, id: namespaceId },
1562
+ { nodeKind: PostgresSchemaNodeKind.table, id: tableName },
1563
+ ];
1564
+ }
1565
+
1566
+ /** A policy's dependency chain onto one of the roles it grants to. */
1567
+ function postgresRoleDependsOn(role: string): SchemaNodeRef {
1568
+ return [
1569
+ { nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
1570
+ { nodeKind: PostgresSchemaNodeKind.role, id: role },
1571
+ ];
1572
+ }
1573
+
1574
+ /**
1575
+ * The chains from a table-child object (foreign key, index, unique, primary
1576
+ * key) to each of the own columns it is built on — the introspection-side
1577
+ * mirror of `contractToPostgresDatabaseSchemaNode`'s `columnDependsOn`. An
1578
+ * object is dropped before the columns it covers.
1579
+ */
1580
+ function postgresColumnDependsOn(
1581
+ namespaceId: string,
1582
+ tableName: string,
1583
+ columns: readonly string[],
1584
+ ): SchemaNodeRef[] {
1585
+ return columns.map((column) => [
1586
+ ...postgresTableDependsOn(namespaceId, tableName),
1587
+ { nodeKind: RelationalSchemaNodeKind.column, id: `column:${column}` },
1588
+ ]);
1589
+ }
1590
+
1505
1591
  /**
1506
1592
  * Groups an array of objects by a specified key.
1507
1593
  * Returns a Map for O(1) lookup by group key.
@@ -5,18 +5,23 @@ import {
5
5
  type AggregateExpr,
6
6
  type AnyExpression,
7
7
  type AnyFromSource,
8
+ type AnyJsonValueProjection,
8
9
  type AnyParamRef,
9
10
  type AnyQueryAst,
10
11
  type BinaryExpr,
12
+ type CaseExpr,
13
+ type CastExpr,
11
14
  type ColumnRef,
12
15
  collectOrderedParamRefs,
13
16
  type DeleteAst,
17
+ type FunctionCallExpr,
14
18
  type InsertAst,
15
19
  type InsertValue,
16
20
  type JoinAst,
17
21
  type JoinOnExpr,
18
22
  type JsonArrayAggExpr,
19
23
  type JsonObjectExpr,
24
+ type JsonValueProjectionVisitor,
20
25
  type ListExpression,
21
26
  LiteralExpr,
22
27
  type LoweredParam,
@@ -121,6 +126,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
121
126
  return typeof value === 'object' && value !== null;
122
127
  }
123
128
 
129
+ function unreachableKind(value: never): string {
130
+ const candidate: unknown = value;
131
+ if (
132
+ typeof candidate === 'object' &&
133
+ candidate !== null &&
134
+ 'kind' in candidate &&
135
+ typeof candidate.kind === 'string'
136
+ ) {
137
+ return candidate.kind;
138
+ }
139
+ return 'unknown';
140
+ }
141
+
124
142
  /**
125
143
  * Per-render carrier threaded through every helper. Bundles the param-index map (for `$N` numbering) and the assembled-stack `codecLookup` (for cast policy at the `renderTypedParam` chokepoint). Carrying both on a single value keeps helper signatures stable.
126
144
  */
@@ -169,9 +187,7 @@ export function renderLoweredSql(
169
187
  break;
170
188
  // v8 ignore next 4
171
189
  default:
172
- throw new Error(
173
- `Unsupported AST node kind: ${(node satisfies never as { kind: string }).kind}`,
174
- );
190
+ throw new Error(`Unsupported AST node kind: ${unreachableKind(node)}`);
175
191
  }
176
192
 
177
193
  return Object.freeze({ sql, params: Object.freeze(params) });
@@ -465,13 +481,17 @@ function renderSource(
465
481
  case 'function-source': {
466
482
  const args = node.args.map((arg) => renderExpr(arg, contract, pim)).join(', ');
467
483
  const call = `${node.fn}(${args})`;
468
- return node.alias !== undefined ? `${call} AS ${quoteIdentifier(node.alias)}` : call;
484
+ const ordinality = node.ordinality ? ' WITH ORDINALITY' : '';
485
+ const alias = node.alias === undefined ? '' : ` AS ${quoteIdentifier(node.alias)}`;
486
+ const columnAliases =
487
+ node.columnAliases === undefined
488
+ ? ''
489
+ : `(${node.columnAliases.map((column) => quoteIdentifier(column)).join(', ')})`;
490
+ return `${call}${ordinality}${alias}${columnAliases}`;
469
491
  }
470
492
  // v8 ignore next 4
471
493
  default:
472
- throw new Error(
473
- `Unsupported source node kind: ${(node satisfies never as { kind: string }).kind}`,
474
- );
494
+ throw new Error(`Unsupported source node kind: ${unreachableKind(node)}`);
475
495
  }
476
496
  }
477
497
 
@@ -518,6 +538,9 @@ function isAtomicExpressionKind(kind: AnyExpression['kind']): boolean {
518
538
  case 'literal':
519
539
  case 'aggregate':
520
540
  case 'window-func':
541
+ case 'function-call':
542
+ case 'cast':
543
+ case 'case':
521
544
  case 'json-object':
522
545
  case 'json-array-agg':
523
546
  case 'list':
@@ -644,6 +667,44 @@ function renderWindowFuncExpr(
644
667
  return `${fn}(${args}) OVER (${over})`;
645
668
  }
646
669
 
670
+ function renderFunctionCallExpr(
671
+ expr: FunctionCallExpr,
672
+ contract: PostgresContract,
673
+ pim: ParamIndexMap,
674
+ ): string {
675
+ const args = expr.args.map((arg) => renderExpr(arg, contract, pim)).join(', ');
676
+ return `${expr.fn}(${args})`;
677
+ }
678
+
679
+ function renderCastExpr(expr: CastExpr, contract: PostgresContract, pim: ParamIndexMap): string {
680
+ return `CAST(${renderExpr(expr.expr, contract, pim)} AS ${expr.targetType})`;
681
+ }
682
+
683
+ function renderCaseExpr(expr: CaseExpr, contract: PostgresContract, pim: ParamIndexMap): string {
684
+ const branches = expr.branches
685
+ .map(
686
+ (branch) =>
687
+ `WHEN ${renderExpr(branch.condition, contract, pim)} THEN ${renderExpr(branch.value, contract, pim)}`,
688
+ )
689
+ .join(' ');
690
+ const elseClause =
691
+ expr.elseExpr === undefined ? '' : ` ELSE ${renderExpr(expr.elseExpr, contract, pim)}`;
692
+ return `CASE ${branches}${elseClause} END`;
693
+ }
694
+
695
+ function renderJsonValueProjection(
696
+ projection: AnyJsonValueProjection,
697
+ contract: PostgresContract,
698
+ pim: ParamIndexMap,
699
+ ): string {
700
+ const visitor: JsonValueProjectionVisitor<string> = {
701
+ codec: ({ value }) => renderExpr(value, contract, pim),
702
+ native: ({ value }) => renderExpr(value, contract, pim),
703
+ document: ({ value }) => renderExpr(value, contract, pim),
704
+ };
705
+ return projection.accept(visitor);
706
+ }
707
+
647
708
  function renderJsonObjectExpr(
648
709
  expr: JsonObjectExpr,
649
710
  contract: PostgresContract,
@@ -652,10 +713,7 @@ function renderJsonObjectExpr(
652
713
  const args = expr.entries
653
714
  .flatMap((entry): [string, string] => {
654
715
  const key = `'${escapeLiteral(entry.key)}'`;
655
- if (entry.value.kind === 'literal') {
656
- return [key, renderLiteral(entry.value)];
657
- }
658
- return [key, renderExpr(entry.value, contract, pim)];
716
+ return [key, renderJsonValueProjection(entry.value, contract, pim)];
659
717
  })
660
718
  .join(', ');
661
719
  return `json_build_object(${args})`;
@@ -680,7 +738,7 @@ function renderJsonArrayAggExpr(
680
738
  expr.orderBy && expr.orderBy.length > 0
681
739
  ? ` ORDER BY ${renderOrderByItems(expr.orderBy, contract, pim)}`
682
740
  : '';
683
- const aggregated = `json_agg(${renderExpr(expr.expr, contract, pim)}${aggregateOrderBy})`;
741
+ const aggregated = `json_agg(${renderJsonValueProjection(expr.expr, contract, pim)}${aggregateOrderBy})`;
684
742
  if (expr.onEmpty === 'emptyArray') {
685
743
  return `coalesce(${aggregated}, json_build_array())`;
686
744
  }
@@ -702,6 +760,12 @@ function renderExpr(expr: AnyExpression, contract: PostgresContract, pim: ParamI
702
760
  return renderAggregateExpr(node, contract, pim);
703
761
  case 'window-func':
704
762
  return renderWindowFuncExpr(node, contract, pim);
763
+ case 'function-call':
764
+ return renderFunctionCallExpr(node, contract, pim);
765
+ case 'cast':
766
+ return renderCastExpr(node, contract, pim);
767
+ case 'case':
768
+ return renderCaseExpr(node, contract, pim);
705
769
  case 'json-object':
706
770
  return renderJsonObjectExpr(node, contract, pim);
707
771
  case 'json-array-agg':
@@ -738,9 +802,7 @@ function renderExpr(expr: AnyExpression, contract: PostgresContract, pim: ParamI
738
802
  return renderRawExpr(node, contract, pim);
739
803
  // v8 ignore next 4
740
804
  default:
741
- throw new Error(
742
- `Unsupported expression node kind: ${(node satisfies never as { kind: string }).kind}`,
743
- );
805
+ throw new Error(`Unsupported expression node kind: ${unreachableKind(node)}`);
744
806
  }
745
807
  }
746
808
 
@@ -912,9 +974,7 @@ function renderInsertValue(
912
974
  return renderExpr(value, contract, pim);
913
975
  // v8 ignore next 4
914
976
  default:
915
- throw new Error(
916
- `Unsupported value node in INSERT: ${(value satisfies never as { kind: string }).kind}`,
917
- );
977
+ throw new Error(`Unsupported value node in INSERT: ${unreachableKind(value)}`);
918
978
  }
919
979
  }
920
980
 
@@ -979,9 +1039,7 @@ function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamInde
979
1039
  }
980
1040
  // v8 ignore next 4
981
1041
  default:
982
- throw new Error(
983
- `Unsupported onConflict action: ${(action satisfies never as { kind: string }).kind}`,
984
- );
1042
+ throw new Error(`Unsupported onConflict action: ${unreachableKind(action)}`);
985
1043
  }
986
1044
  })()
987
1045
  : '';