@prisma-next/adapter-postgres 0.14.0 → 0.15.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.
- package/dist/{adapter-CwkcdpM_.mjs → adapter-Bh8GA6sC.mjs} +4 -3
- package/dist/adapter-Bh8GA6sC.mjs.map +1 -0
- package/dist/adapter.d.mts +1 -1
- package/dist/adapter.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/{control-adapter-Dspz5uKp.mjs → control-adapter-aFfSI2bK.mjs} +427 -120
- package/dist/control-adapter-aFfSI2bK.mjs.map +1 -0
- package/dist/control.d.mts +23 -12
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +46 -107
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-DOgMfoqm.mjs → descriptor-meta-jMtx881n.mjs} +12 -4
- package/dist/descriptor-meta-jMtx881n.mjs.map +1 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.mjs +2 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-KXRwRZU8.d.mts → types-DkGeLfUp.d.mts} +3 -6
- package/dist/types-DkGeLfUp.d.mts.map +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +26 -25
- package/src/core/adapter.ts +1 -0
- package/src/core/codec-lookup.ts +2 -1
- package/src/core/control-adapter.ts +611 -152
- package/src/core/control-mutation-defaults.ts +61 -190
- package/src/core/descriptor-meta.ts +4 -0
- package/src/core/marker-ledger.ts +15 -0
- package/src/core/sql-renderer.ts +32 -6
- package/src/core/types.ts +2 -3
- package/src/exports/runtime.ts +2 -5
- package/dist/adapter-CwkcdpM_.mjs.map +0 -1
- package/dist/control-adapter-Dspz5uKp.mjs.map +0 -1
- package/dist/descriptor-meta-DOgMfoqm.mjs.map +0 -1
- package/dist/types-KXRwRZU8.d.mts.map +0 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
|
|
8
8
|
import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
|
|
9
9
|
import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
|
|
10
|
-
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
10
|
+
import { APP_SPACE_ID, type SchemaNodeRef } from '@prisma-next/framework-components/control';
|
|
11
11
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
12
12
|
import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
|
|
13
13
|
import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
|
|
@@ -28,16 +28,15 @@ import type {
|
|
|
28
28
|
} from '@prisma-next/sql-relational-core/ast';
|
|
29
29
|
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
30
30
|
import type {
|
|
31
|
-
|
|
31
|
+
PrimaryKeyInput,
|
|
32
32
|
SqlCheckConstraintIRInput,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
SqlColumnIRInput,
|
|
34
|
+
SqlForeignKeyIRInput,
|
|
35
|
+
SqlIndexIRInput,
|
|
36
36
|
SqlReferentialAction,
|
|
37
|
-
|
|
38
|
-
SqlTableIR,
|
|
39
|
-
SqlUniqueIR,
|
|
37
|
+
SqlUniqueIRInput,
|
|
40
38
|
} from '@prisma-next/sql-schema-ir/types';
|
|
39
|
+
import { RelationalSchemaNodeKind } from '@prisma-next/sql-schema-ir/types';
|
|
41
40
|
import {
|
|
42
41
|
buildControlTableBootstrapQueries,
|
|
43
42
|
buildSignMarkerBootstrapQueries,
|
|
@@ -45,14 +44,32 @@ import {
|
|
|
45
44
|
import type {
|
|
46
45
|
AddColumnAction,
|
|
47
46
|
AlterTableActionVisitor,
|
|
47
|
+
DropDefaultAction,
|
|
48
|
+
PostgresAlterPolicyRename,
|
|
48
49
|
PostgresAlterTable,
|
|
50
|
+
PostgresCreatePolicy,
|
|
49
51
|
PostgresCreateSchema,
|
|
50
52
|
PostgresCreateTable,
|
|
53
|
+
PostgresCreateType,
|
|
51
54
|
PostgresDdlNode,
|
|
55
|
+
PostgresDisableRowLevelSecurity,
|
|
56
|
+
PostgresDropPolicy,
|
|
57
|
+
PostgresDropType,
|
|
58
|
+
RlsPolicyOperation,
|
|
52
59
|
} from '@prisma-next/target-postgres/ddl';
|
|
53
60
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
54
61
|
import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
|
|
62
|
+
import { parseRlsPolicyWireName } from '@prisma-next/target-postgres/rls-canonicalize';
|
|
55
63
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
64
|
+
import {
|
|
65
|
+
PostgresDatabaseSchemaNode,
|
|
66
|
+
PostgresNamespaceSchemaNode,
|
|
67
|
+
PostgresNativeEnumSchemaNode,
|
|
68
|
+
PostgresPolicySchemaNode,
|
|
69
|
+
PostgresRoleSchemaNode,
|
|
70
|
+
PostgresSchemaNodeKind,
|
|
71
|
+
PostgresTableSchemaNode,
|
|
72
|
+
} from '@prisma-next/target-postgres/types';
|
|
56
73
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
57
74
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
58
75
|
import { encodeControlQueryParams } from './control-codecs';
|
|
@@ -60,6 +77,7 @@ import {
|
|
|
60
77
|
execute,
|
|
61
78
|
infoSchemaTables,
|
|
62
79
|
ledger,
|
|
80
|
+
ledgerContract,
|
|
63
81
|
ledgerReadShape,
|
|
64
82
|
marker,
|
|
65
83
|
mergeInvariants,
|
|
@@ -429,7 +447,11 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
429
447
|
}
|
|
430
448
|
|
|
431
449
|
/**
|
|
432
|
-
* Appends a ledger entry for `space`.
|
|
450
|
+
* Appends a ledger entry for `space`. When the edge carries a
|
|
451
|
+
* destination contract snapshot, the content-addressed
|
|
452
|
+
* `prisma_contract.contract` store is populated first (keyed by the
|
|
453
|
+
* destination hash, DO NOTHING on revisit) so a reader never sees a
|
|
454
|
+
* ledger row whose stored destination contract is missing. See the
|
|
433
455
|
* `SqlControlAdapter.writeLedgerEntry` contract.
|
|
434
456
|
*/
|
|
435
457
|
async writeLedgerEntry(
|
|
@@ -442,10 +464,23 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
442
464
|
readonly migrationName: string;
|
|
443
465
|
readonly migrationHash: string;
|
|
444
466
|
readonly operations: readonly unknown[];
|
|
467
|
+
readonly destinationContractJson?: unknown;
|
|
445
468
|
},
|
|
446
469
|
): Promise<void> {
|
|
470
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
471
|
+
if (entry.destinationContractJson !== undefined) {
|
|
472
|
+
await execute(
|
|
473
|
+
lower,
|
|
474
|
+
driver,
|
|
475
|
+
ledgerContract
|
|
476
|
+
.upsert({ core_hash: entry.to, contract_json: entry.destinationContractJson })
|
|
477
|
+
.onConflict(ledgerContract.core_hash)
|
|
478
|
+
.doNothing()
|
|
479
|
+
.build(),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
447
482
|
await execute(
|
|
448
|
-
|
|
483
|
+
lower,
|
|
449
484
|
driver,
|
|
450
485
|
ledger
|
|
451
486
|
.insert({
|
|
@@ -548,26 +583,51 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
548
583
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
549
584
|
contract?: unknown,
|
|
550
585
|
schema = 'public',
|
|
551
|
-
): Promise<
|
|
586
|
+
): Promise<PostgresDatabaseSchemaNode> {
|
|
552
587
|
const declaredNamespaces = extractContractNamespaceIds(contract);
|
|
553
|
-
const
|
|
588
|
+
const resolvedSchemas =
|
|
554
589
|
declaredNamespaces.length > 0
|
|
555
|
-
? await this.
|
|
556
|
-
:
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
590
|
+
? await this.resolveNamespaceSchemas(driver, declaredNamespaces)
|
|
591
|
+
: [schema];
|
|
592
|
+
|
|
593
|
+
// Walk schemas sequentially: every introspectSchema call shares the one
|
|
594
|
+
// control connection, so a parallel walk only serialises behind the wire
|
|
595
|
+
// protocol and trips pg's "already executing a query" deprecation.
|
|
596
|
+
const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};
|
|
597
|
+
let pgVersion = 'unknown';
|
|
598
|
+
for (const resolved of resolvedSchemas) {
|
|
599
|
+
const { namespace, pgVersion: version } = await this.introspectSchema(driver, resolved);
|
|
600
|
+
namespaces[resolved] = namespace;
|
|
601
|
+
pgVersion = version;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const roles = await this.introspectRoles(driver);
|
|
561
605
|
const existingSchemas = await this.listExistingSchemas(driver);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
606
|
+
return new PostgresDatabaseSchemaNode({
|
|
607
|
+
namespaces,
|
|
608
|
+
roles,
|
|
609
|
+
existingSchemas,
|
|
610
|
+
pgVersion,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Reads cluster-scoped database roles. Roles are not schema-qualified, so
|
|
616
|
+
* this is queried once for the whole database rather than per namespace.
|
|
617
|
+
*/
|
|
618
|
+
private async introspectRoles(
|
|
619
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
620
|
+
): Promise<readonly PostgresRoleSchemaNode[]> {
|
|
621
|
+
const rolesResult = await driver.query<{ rolname: string }>(
|
|
622
|
+
`SELECT rolname
|
|
623
|
+
FROM pg_catalog.pg_roles
|
|
624
|
+
WHERE rolname NOT LIKE 'pg_%'
|
|
625
|
+
AND rolname != 'postgres'
|
|
626
|
+
ORDER BY rolname`,
|
|
627
|
+
);
|
|
628
|
+
return rolesResult.rows.map(
|
|
629
|
+
(row) => new PostgresRoleSchemaNode({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
|
|
630
|
+
);
|
|
571
631
|
}
|
|
572
632
|
|
|
573
633
|
/**
|
|
@@ -593,16 +653,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
593
653
|
}
|
|
594
654
|
|
|
595
655
|
/**
|
|
596
|
-
*
|
|
597
|
-
* the connection's `current_schema()
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
656
|
+
* Resolves the declared namespace ids to their live DDL schema names,
|
|
657
|
+
* mapping `UNBOUND_NAMESPACE_ID` to the connection's `current_schema()`
|
|
658
|
+
* and de-duplicating. The caller introspects one namespace node per
|
|
659
|
+
* resolved schema — there is no flat cross-schema merge, so two schemas
|
|
660
|
+
* holding a same-named table no longer collide.
|
|
601
661
|
*/
|
|
602
|
-
private async
|
|
662
|
+
private async resolveNamespaceSchemas(
|
|
603
663
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
604
664
|
namespaceIds: readonly string[],
|
|
605
|
-
): Promise<
|
|
665
|
+
): Promise<readonly string[]> {
|
|
606
666
|
const resolvedSchemas: string[] = [];
|
|
607
667
|
for (const id of namespaceIds) {
|
|
608
668
|
if (id === UNBOUND_NAMESPACE_ID) {
|
|
@@ -614,46 +674,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
614
674
|
resolvedSchemas.push(id);
|
|
615
675
|
}
|
|
616
676
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// Walk schemas sequentially: every introspectSchema call shares the one
|
|
620
|
-
// control connection, so a parallel walk only serialises behind the wire
|
|
621
|
-
// protocol and trips pg's "already executing a query" deprecation.
|
|
622
|
-
const perSchema: SqlSchemaIR[] = [];
|
|
623
|
-
for (const schema of uniqueSchemas) {
|
|
624
|
-
perSchema.push(await this.introspectSchema(driver, schema));
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const mergedTables: Record<string, SqlTableIR> = {};
|
|
628
|
-
for (const ir of perSchema) {
|
|
629
|
-
for (const [tableName, table] of Object.entries(ir.tables)) {
|
|
630
|
-
mergedTables[tableName] = table;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const firstAnnotations = perSchema[0]?.annotations;
|
|
635
|
-
const firstPg =
|
|
636
|
-
blindCast<Record<string, unknown> | undefined, 'pg annotation envelope index slot'>(
|
|
637
|
-
firstAnnotations?.['pg'],
|
|
638
|
-
) ?? {};
|
|
639
|
-
return {
|
|
640
|
-
tables: mergedTables,
|
|
641
|
-
...ifDefined('annotations', {
|
|
642
|
-
...firstAnnotations,
|
|
643
|
-
pg: { ...firstPg },
|
|
644
|
-
}),
|
|
645
|
-
};
|
|
677
|
+
return Array.from(new Set(resolvedSchemas));
|
|
646
678
|
}
|
|
647
679
|
|
|
648
680
|
/**
|
|
649
|
-
* Introspects a single Postgres schema and returns
|
|
650
|
-
*
|
|
681
|
+
* Introspects a single Postgres schema and returns the namespace node for
|
|
682
|
+
* that schema (its tables, their policies, and its native enum type names),
|
|
683
|
+
* alongside the cluster-scoped Postgres version. Used by `introspect` as
|
|
651
684
|
* the per-namespace walk.
|
|
652
685
|
*/
|
|
653
686
|
private async introspectSchema(
|
|
654
687
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
655
688
|
schema: string,
|
|
656
|
-
): Promise<
|
|
689
|
+
): Promise<{ readonly namespace: PostgresNamespaceSchemaNode; readonly pgVersion: string }> {
|
|
657
690
|
// Issue the schema-wide queries one at a time. A single control connection
|
|
658
691
|
// serialises queries anyway, so Promise.all buys no parallelism here and
|
|
659
692
|
// makes pg emit a "client is already executing a query" deprecation. One
|
|
@@ -706,7 +739,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
706
739
|
ORDER BY c.table_name, c.ordinal_position`,
|
|
707
740
|
[schema],
|
|
708
741
|
);
|
|
709
|
-
// Query all primary keys for all tables in schema
|
|
742
|
+
// Query all primary keys for all tables in schema. Reads pg_catalog, not
|
|
743
|
+
// information_schema: `information_schema.table_constraints` hides
|
|
744
|
+
// constraints on tables where the connecting role's only privilege is
|
|
745
|
+
// SELECT (its filter grants visibility for INSERT/UPDATE/DELETE/TRUNCATE/
|
|
746
|
+
// REFERENCES/TRIGGER — not SELECT), so a SELECT-only table introspects
|
|
747
|
+
// with all its columns but zero constraints and verify reports every
|
|
748
|
+
// declared constraint as missing. pg_catalog is not privilege-filtered.
|
|
710
749
|
const pkResult = await driver.query<{
|
|
711
750
|
table_name: string;
|
|
712
751
|
constraint_name: string;
|
|
@@ -714,24 +753,29 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
714
753
|
ordinal_position: number;
|
|
715
754
|
}>(
|
|
716
755
|
`SELECT
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
FROM
|
|
722
|
-
JOIN
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
AND
|
|
728
|
-
|
|
756
|
+
cl.relname AS table_name,
|
|
757
|
+
con.conname AS constraint_name,
|
|
758
|
+
a.attname AS column_name,
|
|
759
|
+
k.ord AS ordinal_position
|
|
760
|
+
FROM pg_catalog.pg_constraint con
|
|
761
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
|
|
762
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
|
763
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
764
|
+
JOIN pg_catalog.pg_attribute a
|
|
765
|
+
ON a.attrelid = con.conrelid
|
|
766
|
+
AND a.attnum = k.attnum
|
|
767
|
+
WHERE ns.nspname = $1
|
|
768
|
+
AND con.contype = 'p'
|
|
769
|
+
ORDER BY cl.relname, k.ord`,
|
|
729
770
|
[schema],
|
|
730
771
|
);
|
|
731
|
-
// Query all foreign keys for all tables in schema, including referential
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
772
|
+
// Query all foreign keys for all tables in schema, including referential
|
|
773
|
+
// actions. Reads pg_catalog only (see the primary-key query above for why
|
|
774
|
+
// information_schema is unusable here); `unnest(conkey) WITH ORDINALITY`
|
|
775
|
+
// pairs source and referenced columns positionally, so composite FKs
|
|
776
|
+
// round-trip in declared order. The referential-action CASEs mirror
|
|
777
|
+
// pg_constraint's confdeltype/confupdtype encoding and produce the same
|
|
778
|
+
// rule strings information_schema.referential_constraints reports.
|
|
735
779
|
const fkResult = await driver.query<{
|
|
736
780
|
table_name: string;
|
|
737
781
|
constraint_name: string;
|
|
@@ -744,41 +788,48 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
744
788
|
update_rule: string;
|
|
745
789
|
}>(
|
|
746
790
|
`SELECT
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
791
|
+
cl.relname AS table_name,
|
|
792
|
+
con.conname AS constraint_name,
|
|
793
|
+
a.attname AS column_name,
|
|
794
|
+
k.ord AS ordinal_position,
|
|
751
795
|
ref_ns.nspname AS referenced_table_schema,
|
|
752
796
|
ref_cl.relname AS referenced_table_name,
|
|
753
797
|
ref_att.attname AS referenced_column_name,
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
798
|
+
CASE con.confdeltype
|
|
799
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
800
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
801
|
+
WHEN 'c' THEN 'CASCADE'
|
|
802
|
+
WHEN 'n' THEN 'SET NULL'
|
|
803
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
804
|
+
END AS delete_rule,
|
|
805
|
+
CASE con.confupdtype
|
|
806
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
807
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
808
|
+
WHEN 'c' THEN 'CASCADE'
|
|
809
|
+
WHEN 'n' THEN 'SET NULL'
|
|
810
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
811
|
+
END AS update_rule
|
|
812
|
+
FROM pg_catalog.pg_constraint con
|
|
813
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
|
|
814
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
|
815
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
816
|
+
JOIN pg_catalog.pg_attribute a
|
|
817
|
+
ON a.attrelid = con.conrelid
|
|
818
|
+
AND a.attnum = k.attnum
|
|
766
819
|
JOIN pg_catalog.pg_class ref_cl
|
|
767
|
-
ON ref_cl.oid =
|
|
820
|
+
ON ref_cl.oid = con.confrelid
|
|
768
821
|
JOIN pg_catalog.pg_namespace ref_ns
|
|
769
822
|
ON ref_ns.oid = ref_cl.relnamespace
|
|
770
823
|
JOIN pg_catalog.pg_attribute ref_att
|
|
771
|
-
ON ref_att.attrelid =
|
|
772
|
-
AND ref_att.attnum =
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
WHERE tc.table_schema = $1
|
|
777
|
-
AND tc.constraint_type = 'FOREIGN KEY'
|
|
778
|
-
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
824
|
+
ON ref_att.attrelid = con.confrelid
|
|
825
|
+
AND ref_att.attnum = con.confkey[k.ord]
|
|
826
|
+
WHERE ns.nspname = $1
|
|
827
|
+
AND con.contype = 'f'
|
|
828
|
+
ORDER BY cl.relname, con.conname, k.ord`,
|
|
779
829
|
[schema],
|
|
780
830
|
);
|
|
781
|
-
// Query all unique constraints for all tables in schema (excluding PKs)
|
|
831
|
+
// Query all unique constraints for all tables in schema (excluding PKs).
|
|
832
|
+
// Reads pg_catalog (see the primary-key query above).
|
|
782
833
|
const uniqueResult = await driver.query<{
|
|
783
834
|
table_name: string;
|
|
784
835
|
constraint_name: string;
|
|
@@ -786,18 +837,20 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
786
837
|
ordinal_position: number;
|
|
787
838
|
}>(
|
|
788
839
|
`SELECT
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
FROM
|
|
794
|
-
JOIN
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
AND
|
|
800
|
-
|
|
840
|
+
cl.relname AS table_name,
|
|
841
|
+
con.conname AS constraint_name,
|
|
842
|
+
a.attname AS column_name,
|
|
843
|
+
k.ord AS ordinal_position
|
|
844
|
+
FROM pg_catalog.pg_constraint con
|
|
845
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
|
|
846
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
|
847
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
848
|
+
JOIN pg_catalog.pg_attribute a
|
|
849
|
+
ON a.attrelid = con.conrelid
|
|
850
|
+
AND a.attnum = k.attnum
|
|
851
|
+
WHERE ns.nspname = $1
|
|
852
|
+
AND con.contype = 'u'
|
|
853
|
+
ORDER BY cl.relname, con.conname, k.ord`,
|
|
801
854
|
[schema],
|
|
802
855
|
);
|
|
803
856
|
// Query all indexes for all tables in schema (excluding constraints).
|
|
@@ -842,10 +895,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
842
895
|
WHERE i.schemaname = $1
|
|
843
896
|
AND NOT EXISTS (
|
|
844
897
|
SELECT 1
|
|
845
|
-
FROM
|
|
846
|
-
WHERE
|
|
847
|
-
AND
|
|
848
|
-
AND tc.constraint_name = i.indexname
|
|
898
|
+
FROM pg_catalog.pg_constraint con
|
|
899
|
+
WHERE con.conindid = ic.oid
|
|
900
|
+
AND con.contype IN ('p', 'u', 'x')
|
|
849
901
|
)
|
|
850
902
|
ORDER BY i.tablename, i.indexname, k.ord`,
|
|
851
903
|
[schema],
|
|
@@ -895,13 +947,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
895
947
|
constraints.add(row.constraint_name);
|
|
896
948
|
}
|
|
897
949
|
|
|
898
|
-
const
|
|
950
|
+
const tableInputs: Record<
|
|
951
|
+
string,
|
|
952
|
+
{
|
|
953
|
+
name: string;
|
|
954
|
+
columns: Record<string, SqlColumnIRInput>;
|
|
955
|
+
primaryKey?: PrimaryKeyInput;
|
|
956
|
+
foreignKeys: readonly SqlForeignKeyIRInput[];
|
|
957
|
+
uniques: readonly SqlUniqueIRInput[];
|
|
958
|
+
indexes: readonly SqlIndexIRInput[];
|
|
959
|
+
checks?: SqlCheckConstraintIRInput[];
|
|
960
|
+
}
|
|
961
|
+
> = {};
|
|
899
962
|
|
|
900
963
|
for (const tableRow of tablesResult.rows) {
|
|
901
964
|
const tableName = tableRow.table_name;
|
|
902
965
|
|
|
903
966
|
// Process columns for this table
|
|
904
|
-
const columns: Record<string,
|
|
967
|
+
const columns: Record<string, SqlColumnIRInput> = {};
|
|
905
968
|
for (const colRow of columnsByTable.get(tableName) ?? []) {
|
|
906
969
|
let nativeType = colRow.udt_name;
|
|
907
970
|
const formattedType = colRow.formatted_type
|
|
@@ -927,11 +990,36 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
927
990
|
nativeType = colRow.udt_name || colRow.data_type;
|
|
928
991
|
}
|
|
929
992
|
|
|
993
|
+
// Postgres reports array columns as data_type='ARRAY'; the element type
|
|
994
|
+
// is the `nativeType` string minus the trailing `[]`. Strip the suffix,
|
|
995
|
+
// normalize the element type to the canonical form (e.g. `integer` →
|
|
996
|
+
// `int4`), and record `many: true` so introspection consumers (verifier,
|
|
997
|
+
// psl-contract-infer) can reconstruct the full array type as needed.
|
|
998
|
+
const many = nativeType.endsWith('[]') ? true : undefined;
|
|
999
|
+
if (many) {
|
|
1000
|
+
nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Resolved values comparable against the contract-derived expected
|
|
1004
|
+
// side: the normalized full native type (`[]` appended for arrays)
|
|
1005
|
+
// and the structured parse of the raw default. Raw fields stay
|
|
1006
|
+
// untouched alongside — the relational walk still reads and
|
|
1007
|
+
// normalizes them itself.
|
|
1008
|
+
const resolvedNativeType = `${normalizeSchemaNativeType(nativeType)}${many ? '[]' : ''}`;
|
|
1009
|
+
const rawDefault = colRow.column_default ?? undefined;
|
|
930
1010
|
columns[colRow.column_name] = {
|
|
931
1011
|
name: colRow.column_name,
|
|
932
1012
|
nativeType,
|
|
933
1013
|
nullable: colRow.is_nullable === 'YES',
|
|
934
|
-
...ifDefined('default',
|
|
1014
|
+
...ifDefined('default', rawDefault),
|
|
1015
|
+
...ifDefined('many', many),
|
|
1016
|
+
resolvedNativeType,
|
|
1017
|
+
...ifDefined(
|
|
1018
|
+
'resolvedDefault',
|
|
1019
|
+
rawDefault !== undefined
|
|
1020
|
+
? parsePostgresDefault(rawDefault, resolvedNativeType)
|
|
1021
|
+
: undefined,
|
|
1022
|
+
),
|
|
935
1023
|
};
|
|
936
1024
|
}
|
|
937
1025
|
|
|
@@ -940,11 +1028,12 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
940
1028
|
const primaryKeyColumns = pkRows
|
|
941
1029
|
.sort((a, b) => a.ordinal_position - b.ordinal_position)
|
|
942
1030
|
.map((row) => row.column_name);
|
|
943
|
-
const primaryKey:
|
|
1031
|
+
const primaryKey: PrimaryKeyInput | undefined =
|
|
944
1032
|
primaryKeyColumns.length > 0
|
|
945
1033
|
? {
|
|
946
1034
|
columns: primaryKeyColumns,
|
|
947
1035
|
...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
|
|
1036
|
+
dependsOn: postgresColumnDependsOn(schema, tableName, primaryKeyColumns),
|
|
948
1037
|
}
|
|
949
1038
|
: undefined;
|
|
950
1039
|
|
|
@@ -978,7 +1067,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
978
1067
|
});
|
|
979
1068
|
}
|
|
980
1069
|
}
|
|
981
|
-
const foreignKeys: readonly
|
|
1070
|
+
const foreignKeys: readonly SqlForeignKeyIRInput[] = Array.from(foreignKeysMap.values()).map(
|
|
982
1071
|
(fk) => ({
|
|
983
1072
|
columns: Object.freeze([...fk.columns]) as readonly string[],
|
|
984
1073
|
referencedTable: fk.referencedTable,
|
|
@@ -987,6 +1076,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
987
1076
|
name: fk.name,
|
|
988
1077
|
...ifDefined('onDelete', mapReferentialAction(fk.deleteRule)),
|
|
989
1078
|
...ifDefined('onUpdate', mapReferentialAction(fk.updateRule)),
|
|
1079
|
+
dependsOn: [
|
|
1080
|
+
postgresTableDependsOn(fk.referencedSchema, fk.referencedTable),
|
|
1081
|
+
...postgresColumnDependsOn(schema, tableName, fk.columns),
|
|
1082
|
+
],
|
|
990
1083
|
}),
|
|
991
1084
|
);
|
|
992
1085
|
|
|
@@ -1008,9 +1101,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1008
1101
|
});
|
|
1009
1102
|
}
|
|
1010
1103
|
}
|
|
1011
|
-
const uniques: readonly
|
|
1104
|
+
const uniques: readonly SqlUniqueIRInput[] = Array.from(uniquesMap.values()).map((uq) => ({
|
|
1012
1105
|
columns: Object.freeze([...uq.columns]) as readonly string[],
|
|
1013
1106
|
name: uq.name,
|
|
1107
|
+
dependsOn: postgresColumnDependsOn(schema, tableName, uq.columns),
|
|
1014
1108
|
}));
|
|
1015
1109
|
|
|
1016
1110
|
// Process indexes
|
|
@@ -1024,8 +1118,22 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1024
1118
|
options: Record<string, string> | undefined;
|
|
1025
1119
|
}
|
|
1026
1120
|
>();
|
|
1121
|
+
// An index with an expression key (e.g. `lower(email)`) reports that
|
|
1122
|
+
// key's row with `attname = null` (Postgres attribute numbers are
|
|
1123
|
+
// <= 0 for expressions, which the LEFT JOIN above can't resolve to a
|
|
1124
|
+
// real column). Every row for that index name is skipped below
|
|
1125
|
+
// rather than only the expression row, so the index never enters
|
|
1126
|
+
// `indexesMap` with a collapsed, misleading column list — a
|
|
1127
|
+
// two-column expression index silently reduced to its one real
|
|
1128
|
+
// column can coincide with an unrelated real single-column index,
|
|
1129
|
+
// and the schema differ's diff-tree node id is derived from the
|
|
1130
|
+
// column tuple (`sql-index/index:<columns>`), so two indexes
|
|
1131
|
+
// colliding on that tuple abort the diff with "duplicate id among
|
|
1132
|
+
// siblings" instead of a normal drift report.
|
|
1133
|
+
const indexNamesWithExpressionKey = new Set<string>();
|
|
1027
1134
|
for (const idxRow of indexesByTable.get(tableName) ?? []) {
|
|
1028
1135
|
if (!idxRow.attname) {
|
|
1136
|
+
indexNamesWithExpressionKey.add(idxRow.indexname);
|
|
1029
1137
|
continue;
|
|
1030
1138
|
}
|
|
1031
1139
|
const existing = indexesMap.get(idxRow.indexname);
|
|
@@ -1046,13 +1154,41 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1046
1154
|
});
|
|
1047
1155
|
}
|
|
1048
1156
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1157
|
+
// Two real indexes can legitimately share the exact same column tuple
|
|
1158
|
+
// on one table (e.g. a unique index and a redundant plain index) —
|
|
1159
|
+
// valid in Postgres, but the schema differ's diff-tree node id for an
|
|
1160
|
+
// index is the column tuple alone (`SqlIndexIR#id`, deliberately —
|
|
1161
|
+
// see its doc comment), so two same-tuple siblings from one
|
|
1162
|
+
// introspection abort the diff with "duplicate id among siblings"
|
|
1163
|
+
// rather than a normal drift report. Keep only one per column tuple:
|
|
1164
|
+
// the unique one when there is a unique/non-unique pair (a unique
|
|
1165
|
+
// index is a strict superset of what a plain index on the same
|
|
1166
|
+
// columns would add), otherwise the first by name for determinism.
|
|
1167
|
+
const survivingIndexes = Array.from(indexesMap.values()).filter(
|
|
1168
|
+
(idx) => !indexNamesWithExpressionKey.has(idx.name),
|
|
1169
|
+
);
|
|
1170
|
+
const bestByColumnTuple = new Map<string, (typeof survivingIndexes)[number]>();
|
|
1171
|
+
for (const idx of survivingIndexes) {
|
|
1172
|
+
const tupleKey = idx.columns.join(',');
|
|
1173
|
+
const existing = bestByColumnTuple.get(tupleKey);
|
|
1174
|
+
if (
|
|
1175
|
+
!existing ||
|
|
1176
|
+
(idx.unique && !existing.unique) ||
|
|
1177
|
+
(idx.unique === existing.unique && idx.name < existing.name)
|
|
1178
|
+
) {
|
|
1179
|
+
bestByColumnTuple.set(tupleKey, idx);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const indexes: readonly SqlIndexIRInput[] = Array.from(bestByColumnTuple.values()).map(
|
|
1183
|
+
(idx) => ({
|
|
1184
|
+
columns: Object.freeze([...idx.columns]),
|
|
1185
|
+
name: idx.name,
|
|
1186
|
+
unique: idx.unique,
|
|
1187
|
+
...(idx.type !== undefined && { type: idx.type }),
|
|
1188
|
+
...(idx.options !== undefined && { options: idx.options }),
|
|
1189
|
+
dependsOn: postgresColumnDependsOn(schema, tableName, idx.columns),
|
|
1190
|
+
}),
|
|
1191
|
+
);
|
|
1056
1192
|
|
|
1057
1193
|
// Process check constraints — parse each predicate into column + value set.
|
|
1058
1194
|
// Only the two shapes emitted by this slice are recognised; free-form
|
|
@@ -1069,7 +1205,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1069
1205
|
}
|
|
1070
1206
|
}
|
|
1071
1207
|
|
|
1072
|
-
|
|
1208
|
+
tableInputs[tableName] = {
|
|
1073
1209
|
name: tableName,
|
|
1074
1210
|
columns,
|
|
1075
1211
|
...ifDefined('primaryKey', primaryKey),
|
|
@@ -1080,29 +1216,114 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1080
1216
|
};
|
|
1081
1217
|
}
|
|
1082
1218
|
|
|
1083
|
-
const nativeEnumResult = await driver.query<{ typname: string }>(
|
|
1084
|
-
`SELECT t.typname
|
|
1219
|
+
const nativeEnumResult = await driver.query<{ typname: string; enumvalues: unknown }>(
|
|
1220
|
+
`SELECT t.typname, array_agg(e.enumlabel ORDER BY e.enumsortorder) AS enumvalues
|
|
1085
1221
|
FROM pg_catalog.pg_type t
|
|
1086
1222
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
1223
|
+
JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
|
|
1087
1224
|
WHERE t.typtype = 'e'
|
|
1088
1225
|
AND n.nspname = $1
|
|
1226
|
+
GROUP BY t.typname
|
|
1089
1227
|
ORDER BY t.typname`,
|
|
1090
1228
|
[schema],
|
|
1091
1229
|
);
|
|
1092
|
-
const
|
|
1230
|
+
const enums = nativeEnumResult.rows.map(
|
|
1231
|
+
(r) =>
|
|
1232
|
+
new PostgresNativeEnumSchemaNode({
|
|
1233
|
+
typeName: r.typname,
|
|
1234
|
+
namespaceId: schema,
|
|
1235
|
+
members: parsePgNameArray(r.enumvalues),
|
|
1236
|
+
}),
|
|
1237
|
+
);
|
|
1238
|
+
const policiesResult = await driver.query<{
|
|
1239
|
+
schemaname: string;
|
|
1240
|
+
tablename: string;
|
|
1241
|
+
policyname: string;
|
|
1242
|
+
cmd: string;
|
|
1243
|
+
roles: string[];
|
|
1244
|
+
qual: string | null;
|
|
1245
|
+
with_check: string | null;
|
|
1246
|
+
permissive: string;
|
|
1247
|
+
}>(
|
|
1248
|
+
`SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
|
|
1249
|
+
FROM pg_catalog.pg_policies
|
|
1250
|
+
WHERE schemaname = $1
|
|
1251
|
+
ORDER BY tablename, policyname`,
|
|
1252
|
+
[schema],
|
|
1253
|
+
);
|
|
1254
|
+
const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
|
|
1255
|
+
for (const row of policiesResult.rows) {
|
|
1256
|
+
const operation = mapPgCmd(row.cmd);
|
|
1257
|
+
const policyRoles = [
|
|
1258
|
+
...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
|
|
1259
|
+
].sort();
|
|
1260
|
+
const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
|
|
1261
|
+
const prefix = parseRlsPolicyWireName(row.policyname)?.prefix ?? row.policyname;
|
|
1262
|
+
const policy = new PostgresPolicySchemaNode({
|
|
1263
|
+
name: row.policyname,
|
|
1264
|
+
prefix,
|
|
1265
|
+
tableName: row.tablename,
|
|
1266
|
+
namespaceId: row.schemaname,
|
|
1267
|
+
operation,
|
|
1268
|
+
roles: policyRoles,
|
|
1269
|
+
...(row.qual !== null ? { using: row.qual } : {}),
|
|
1270
|
+
...(row.with_check !== null ? { withCheck: row.with_check } : {}),
|
|
1271
|
+
permissive,
|
|
1272
|
+
dependsOn: [
|
|
1273
|
+
postgresTableDependsOn(row.schemaname, row.tablename),
|
|
1274
|
+
...policyRoles.map(postgresRoleDependsOn),
|
|
1275
|
+
],
|
|
1276
|
+
});
|
|
1277
|
+
const list = policiesByTable.get(row.tablename) ?? [];
|
|
1278
|
+
list.push(policy);
|
|
1279
|
+
policiesByTable.set(row.tablename, list);
|
|
1280
|
+
}
|
|
1093
1281
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1282
|
+
// RLS enablement is a table attribute (`pg_class.relrowsecurity`), not a
|
|
1283
|
+
// function of the policy set — a table can have RLS on with zero
|
|
1284
|
+
// policies (deny-all) or policies present with RLS off. relkind covers
|
|
1285
|
+
// both plain ('r') and partitioned ('p') tables: the table listing above
|
|
1286
|
+
// (`information_schema.tables`, BASE TABLE) includes partitioned parents,
|
|
1287
|
+
// and Postgres supports RLS on them.
|
|
1288
|
+
//
|
|
1289
|
+
// Kept as a SEPARATE query from the table listing on purpose. Folding
|
|
1290
|
+
// relrowsecurity into the listing would mean replacing
|
|
1291
|
+
// `information_schema.tables` (which filters by the connection role's
|
|
1292
|
+
// grants) with a raw `pg_class` scan (which does not), changing WHICH
|
|
1293
|
+
// tables the introspection returns — a real behavior shift, not a
|
|
1294
|
+
// cleanup, and one the offline golden-diff can't catch (introspection is
|
|
1295
|
+
// live-only). The only cost of two queries is the concurrent-DDL window:
|
|
1296
|
+
// a table listed but missed by this scan defaults (`?? false`) to
|
|
1297
|
+
// RLS-off. That default is fail-safe — the worst case downstream is a
|
|
1298
|
+
// spurious ENABLE (idempotent), never a spurious DISABLE.
|
|
1299
|
+
const rlsEnabledResult = await driver.query<{ tablename: string; rls_enabled: boolean }>(
|
|
1300
|
+
`SELECT c.relname AS tablename, c.relrowsecurity AS rls_enabled
|
|
1301
|
+
FROM pg_catalog.pg_class c
|
|
1302
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
1303
|
+
WHERE n.nspname = $1
|
|
1304
|
+
AND c.relkind IN ('r', 'p')
|
|
1305
|
+
ORDER BY c.relname`,
|
|
1306
|
+
[schema],
|
|
1307
|
+
);
|
|
1308
|
+
const rlsEnabledByTable = new Map<string, boolean>(
|
|
1309
|
+
rlsEnabledResult.rows.map((row) => [row.tablename, row.rls_enabled]),
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1312
|
+
const tables: Record<string, PostgresTableSchemaNode> = {};
|
|
1313
|
+
for (const [tableName, input] of Object.entries(tableInputs)) {
|
|
1314
|
+
tables[tableName] = new PostgresTableSchemaNode({
|
|
1315
|
+
...input,
|
|
1316
|
+
policies: policiesByTable.get(tableName) ?? [],
|
|
1317
|
+
rlsEnabled: rlsEnabledByTable.get(tableName) ?? false,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1101
1320
|
|
|
1102
|
-
|
|
1321
|
+
const namespace = new PostgresNamespaceSchemaNode({
|
|
1322
|
+
schemaName: schema,
|
|
1103
1323
|
tables,
|
|
1104
|
-
|
|
1105
|
-
};
|
|
1324
|
+
nativeEnums: enums,
|
|
1325
|
+
});
|
|
1326
|
+
return { namespace, pgVersion: await this.getPostgresVersion(driver) };
|
|
1106
1327
|
}
|
|
1107
1328
|
|
|
1108
1329
|
/**
|
|
@@ -1117,6 +1338,106 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1117
1338
|
}
|
|
1118
1339
|
}
|
|
1119
1340
|
|
|
1341
|
+
/**
|
|
1342
|
+
* Normalises a `name[]` column value from `pg_policies.roles`.
|
|
1343
|
+
*
|
|
1344
|
+
* The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
|
|
1345
|
+
* `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
|
|
1346
|
+
* literal (`{role1,role2}`) is returned as a string instead of a JS array.
|
|
1347
|
+
* This function accepts either form and returns a plain string array.
|
|
1348
|
+
*
|
|
1349
|
+
* The string branch honors Postgres array-literal quoting: an element
|
|
1350
|
+
* containing a comma, quote, backslash, brace, or significant whitespace is
|
|
1351
|
+
* emitted double-quoted with `\"` / `\\` escapes, and unquoted elements are
|
|
1352
|
+
* whitespace-trimmed — so a label like `in progress` or `say "hi"` parses to
|
|
1353
|
+
* its true value instead of being split or kept escaped.
|
|
1354
|
+
*/
|
|
1355
|
+
export function parsePgNameArray(value: unknown): string[] {
|
|
1356
|
+
if (Array.isArray(value)) {
|
|
1357
|
+
return value.map(String);
|
|
1358
|
+
}
|
|
1359
|
+
if (typeof value !== 'string') {
|
|
1360
|
+
return [];
|
|
1361
|
+
}
|
|
1362
|
+
const trimmed = value.trim();
|
|
1363
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
const inner = trimmed.slice(1, -1);
|
|
1367
|
+
if (inner === '') {
|
|
1368
|
+
return [];
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const elements: string[] = [];
|
|
1372
|
+
let current = '';
|
|
1373
|
+
let inQuotes = false;
|
|
1374
|
+
let wasQuoted = false;
|
|
1375
|
+
const pushCurrent = () => {
|
|
1376
|
+
elements.push(wasQuoted ? current : current.trim());
|
|
1377
|
+
current = '';
|
|
1378
|
+
wasQuoted = false;
|
|
1379
|
+
};
|
|
1380
|
+
let i = 0;
|
|
1381
|
+
while (i < inner.length) {
|
|
1382
|
+
const char = inner.charAt(i);
|
|
1383
|
+
if (inQuotes) {
|
|
1384
|
+
if (char === '\\') {
|
|
1385
|
+
current += inner[i + 1] ?? '';
|
|
1386
|
+
i += 2;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (char === '"') {
|
|
1390
|
+
inQuotes = false;
|
|
1391
|
+
i++;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
current += char;
|
|
1395
|
+
i++;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (char === '"') {
|
|
1399
|
+
inQuotes = true;
|
|
1400
|
+
wasQuoted = true;
|
|
1401
|
+
i++;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (char === ',') {
|
|
1405
|
+
pushCurrent();
|
|
1406
|
+
i++;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
current += char;
|
|
1410
|
+
i++;
|
|
1411
|
+
}
|
|
1412
|
+
// A still-open quote means the literal was malformed (e.g. `{"unterminated}`);
|
|
1413
|
+
// reject rather than emit the partial value.
|
|
1414
|
+
if (inQuotes) {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
pushCurrent();
|
|
1418
|
+
return elements;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
|
|
1423
|
+
* The `pg_policies` view renders the internal command code as an uppercase
|
|
1424
|
+
* English keyword; this function lowercases to match the IR type.
|
|
1425
|
+
*/
|
|
1426
|
+
function mapPgCmd(cmd: string): RlsPolicyOperation {
|
|
1427
|
+
switch (cmd.toUpperCase()) {
|
|
1428
|
+
case 'SELECT':
|
|
1429
|
+
return 'select';
|
|
1430
|
+
case 'INSERT':
|
|
1431
|
+
return 'insert';
|
|
1432
|
+
case 'UPDATE':
|
|
1433
|
+
return 'update';
|
|
1434
|
+
case 'DELETE':
|
|
1435
|
+
return 'delete';
|
|
1436
|
+
default:
|
|
1437
|
+
return 'all';
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1120
1441
|
/**
|
|
1121
1442
|
* Extracts the namespace coordinate ids declared on a contract's storage,
|
|
1122
1443
|
* or returns an empty array when no contract (or no storage / namespaces)
|
|
@@ -1133,6 +1454,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
|
|
|
1133
1454
|
}
|
|
1134
1455
|
|
|
1135
1456
|
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
1457
|
+
if (formattedType.endsWith('[]')) {
|
|
1458
|
+
return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
|
|
1459
|
+
}
|
|
1136
1460
|
if (formattedType === 'integer') {
|
|
1137
1461
|
return 'int4';
|
|
1138
1462
|
}
|
|
@@ -1210,6 +1534,44 @@ function mapReferentialAction(rule: string): SqlReferentialAction | undefined {
|
|
|
1210
1534
|
return mapped;
|
|
1211
1535
|
}
|
|
1212
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* A FK/policy dependency chain, mirroring the shape the expected-side
|
|
1539
|
+
* derivation stamps (`contractToPostgresDatabaseSchemaNode`): the database
|
|
1540
|
+
* root's fixed sentinel id, then the namespace, then the table.
|
|
1541
|
+
*/
|
|
1542
|
+
function postgresTableDependsOn(namespaceId: string, tableName: string): SchemaNodeRef {
|
|
1543
|
+
return [
|
|
1544
|
+
{ nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
|
|
1545
|
+
{ nodeKind: PostgresSchemaNodeKind.namespace, id: namespaceId },
|
|
1546
|
+
{ nodeKind: PostgresSchemaNodeKind.table, id: tableName },
|
|
1547
|
+
];
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** A policy's dependency chain onto one of the roles it grants to. */
|
|
1551
|
+
function postgresRoleDependsOn(role: string): SchemaNodeRef {
|
|
1552
|
+
return [
|
|
1553
|
+
{ nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
|
|
1554
|
+
{ nodeKind: PostgresSchemaNodeKind.role, id: role },
|
|
1555
|
+
];
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* The chains from a table-child object (foreign key, index, unique, primary
|
|
1560
|
+
* key) to each of the own columns it is built on — the introspection-side
|
|
1561
|
+
* mirror of `contractToPostgresDatabaseSchemaNode`'s `columnDependsOn`. An
|
|
1562
|
+
* object is dropped before the columns it covers.
|
|
1563
|
+
*/
|
|
1564
|
+
function postgresColumnDependsOn(
|
|
1565
|
+
namespaceId: string,
|
|
1566
|
+
tableName: string,
|
|
1567
|
+
columns: readonly string[],
|
|
1568
|
+
): SchemaNodeRef[] {
|
|
1569
|
+
return columns.map((column) => [
|
|
1570
|
+
...postgresTableDependsOn(namespaceId, tableName),
|
|
1571
|
+
{ nodeKind: RelationalSchemaNodeKind.column, id: `column:${column}` },
|
|
1572
|
+
]);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1213
1575
|
/**
|
|
1214
1576
|
* Groups an array of objects by a specified key.
|
|
1215
1577
|
* Returns a Map for O(1) lookup by group key.
|
|
@@ -1376,6 +1738,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
|
|
|
1376
1738
|
);
|
|
1377
1739
|
}
|
|
1378
1740
|
|
|
1741
|
+
function pgRenderArrayElement(el: unknown): string {
|
|
1742
|
+
if (el === null) return 'NULL';
|
|
1743
|
+
if (typeof el === 'number' || typeof el === 'boolean') return String(el);
|
|
1744
|
+
if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
|
|
1745
|
+
return `'${escapeLiteral(JSON.stringify(el))}'`;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function pgRenderArrayLiteral(elements: unknown[]): string {
|
|
1749
|
+
if (elements.length === 0) return "'{}'";
|
|
1750
|
+
return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1379
1753
|
function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
1380
1754
|
if (wire === null) return 'NULL';
|
|
1381
1755
|
if (typeof wire === 'boolean') return wire ? 'true' : 'false';
|
|
@@ -1407,6 +1781,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
|
1407
1781
|
.join('');
|
|
1408
1782
|
return `'\\x${hex}'::${nativeType}`;
|
|
1409
1783
|
}
|
|
1784
|
+
if (Array.isArray(wire) && nativeType.endsWith('[]')) {
|
|
1785
|
+
return pgRenderArrayLiteral(wire);
|
|
1786
|
+
}
|
|
1410
1787
|
if (typeof wire === 'object') {
|
|
1411
1788
|
const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
|
|
1412
1789
|
return `${quoted}::${nativeType}`;
|
|
@@ -1477,6 +1854,9 @@ function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
|
|
|
1477
1854
|
}
|
|
1478
1855
|
return sql;
|
|
1479
1856
|
}
|
|
1857
|
+
if (constraint.kind === 'check-expression') {
|
|
1858
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} CHECK (${constraint.expression})`;
|
|
1859
|
+
}
|
|
1480
1860
|
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1481
1861
|
if (constraint.name !== undefined) {
|
|
1482
1862
|
return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
@@ -1512,6 +1892,27 @@ function pgRenderCreateSchema(node: PostgresCreateSchema): SqlExecuteRequest {
|
|
|
1512
1892
|
};
|
|
1513
1893
|
}
|
|
1514
1894
|
|
|
1895
|
+
function pgRenderCreateType(node: PostgresCreateType): SqlExecuteRequest {
|
|
1896
|
+
const typeRef = node.schema
|
|
1897
|
+
? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.name)}`
|
|
1898
|
+
: quoteIdentifier(node.name);
|
|
1899
|
+
const values = node.values.map((value) => `'${escapeLiteral(value)}'`).join(', ');
|
|
1900
|
+
return {
|
|
1901
|
+
sql: `CREATE TYPE ${typeRef} AS ENUM (${values})`,
|
|
1902
|
+
params: [],
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function pgRenderDropType(node: PostgresDropType): SqlExecuteRequest {
|
|
1907
|
+
const typeRef = node.schema
|
|
1908
|
+
? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.name)}`
|
|
1909
|
+
: quoteIdentifier(node.name);
|
|
1910
|
+
return {
|
|
1911
|
+
sql: `DROP TYPE ${typeRef}`,
|
|
1912
|
+
params: [],
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1515
1916
|
async function pgRenderAlterTable(
|
|
1516
1917
|
node: PostgresAlterTable,
|
|
1517
1918
|
codecLookup: CodecLookup,
|
|
@@ -1524,6 +1925,9 @@ async function pgRenderAlterTable(
|
|
|
1524
1925
|
const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
|
|
1525
1926
|
return `ADD COLUMN ${colFragment}`;
|
|
1526
1927
|
},
|
|
1928
|
+
dropDefault(action: DropDefaultAction): Promise<string> {
|
|
1929
|
+
return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
|
|
1930
|
+
},
|
|
1527
1931
|
};
|
|
1528
1932
|
const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
|
|
1529
1933
|
return {
|
|
@@ -1532,6 +1936,53 @@ async function pgRenderAlterTable(
|
|
|
1532
1936
|
};
|
|
1533
1937
|
}
|
|
1534
1938
|
|
|
1939
|
+
const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
|
|
1940
|
+
select: 'SELECT',
|
|
1941
|
+
insert: 'INSERT',
|
|
1942
|
+
update: 'UPDATE',
|
|
1943
|
+
delete: 'DELETE',
|
|
1944
|
+
all: 'ALL',
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
|
|
1948
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1949
|
+
const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
|
|
1950
|
+
const command = POLICY_OPERATION_SQL[node.operation];
|
|
1951
|
+
const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
|
|
1952
|
+
let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
|
|
1953
|
+
if (node.using !== undefined) {
|
|
1954
|
+
sql += ` USING (${node.using})`;
|
|
1955
|
+
}
|
|
1956
|
+
if (node.withCheck !== undefined) {
|
|
1957
|
+
sql += ` WITH CHECK (${node.withCheck})`;
|
|
1958
|
+
}
|
|
1959
|
+
return { sql, params: [] };
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
|
|
1963
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1964
|
+
return {
|
|
1965
|
+
sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
|
|
1966
|
+
params: [],
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function pgRenderAlterPolicyRename(node: PostgresAlterPolicyRename): SqlExecuteRequest {
|
|
1971
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1972
|
+
return {
|
|
1973
|
+
sql: `ALTER POLICY ${quoteIdentifier(node.name)} ON ${tableRef} RENAME TO ${quoteIdentifier(node.newName)}`,
|
|
1974
|
+
params: [],
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function pgRenderDisableRowLevelSecurity(node: PostgresDisableRowLevelSecurity): SqlExecuteRequest {
|
|
1979
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1980
|
+
return {
|
|
1981
|
+
sql: `ALTER TABLE ${tableRef} DISABLE ROW LEVEL SECURITY`,
|
|
1982
|
+
params: [],
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1535
1986
|
async function pgRenderDdlExecuteRequest(
|
|
1536
1987
|
ast: PostgresDdlNode,
|
|
1537
1988
|
codecLookup: CodecLookup,
|
|
@@ -1539,7 +1990,15 @@ async function pgRenderDdlExecuteRequest(
|
|
|
1539
1990
|
const visitor = {
|
|
1540
1991
|
createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
|
|
1541
1992
|
createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
|
|
1993
|
+
createType: (node: PostgresCreateType) => Promise.resolve(pgRenderCreateType(node)),
|
|
1994
|
+
dropType: (node: PostgresDropType) => Promise.resolve(pgRenderDropType(node)),
|
|
1542
1995
|
alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
|
|
1996
|
+
createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
|
|
1997
|
+
dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
|
|
1998
|
+
alterPolicyRename: (node: PostgresAlterPolicyRename) =>
|
|
1999
|
+
Promise.resolve(pgRenderAlterPolicyRename(node)),
|
|
2000
|
+
disableRowLevelSecurity: (node: PostgresDisableRowLevelSecurity) =>
|
|
2001
|
+
Promise.resolve(pgRenderDisableRowLevelSecurity(node)),
|
|
1543
2002
|
};
|
|
1544
2003
|
return ast.accept(visitor);
|
|
1545
2004
|
}
|