@supabase/pg-delta 1.0.0-alpha.21 → 1.0.0-alpha.23
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/core/catalog.diff.js +4 -3
- package/dist/core/catalog.model.d.ts +8 -1
- package/dist/core/catalog.model.js +10 -8
- package/dist/core/expand-replace-dependencies.js +23 -0
- package/dist/core/integrations/filter/flatten.js +13 -0
- package/dist/core/objects/aggregate/aggregate.diff.js +16 -0
- package/dist/core/objects/aggregate/aggregate.model.d.ts +10 -0
- package/dist/core/objects/aggregate/aggregate.model.js +19 -1
- package/dist/core/objects/aggregate/changes/aggregate.base.d.ts +1 -1
- package/dist/core/objects/aggregate/changes/aggregate.security-label.d.ts +28 -0
- package/dist/core/objects/aggregate/changes/aggregate.security-label.js +64 -0
- package/dist/core/objects/aggregate/changes/aggregate.types.d.ts +2 -1
- package/dist/core/objects/base.model.d.ts +8 -0
- package/dist/core/objects/base.model.js +2 -0
- package/dist/core/objects/domain/changes/domain.base.d.ts +1 -1
- package/dist/core/objects/domain/changes/domain.security-label.d.ts +28 -0
- package/dist/core/objects/domain/changes/domain.security-label.js +61 -0
- package/dist/core/objects/domain/changes/domain.types.d.ts +2 -1
- package/dist/core/objects/domain/domain.diff.js +16 -0
- package/dist/core/objects/domain/domain.model.d.ts +10 -0
- package/dist/core/objects/domain/domain.model.js +19 -1
- package/dist/core/objects/event-trigger/changes/event-trigger.base.d.ts +1 -1
- package/dist/core/objects/event-trigger/changes/event-trigger.security-label.d.ts +28 -0
- package/dist/core/objects/event-trigger/changes/event-trigger.security-label.js +61 -0
- package/dist/core/objects/event-trigger/changes/event-trigger.types.d.ts +2 -1
- package/dist/core/objects/event-trigger/event-trigger.diff.js +16 -0
- package/dist/core/objects/event-trigger/event-trigger.model.d.ts +10 -0
- package/dist/core/objects/event-trigger/event-trigger.model.js +19 -1
- package/dist/core/objects/extract-with-retry.d.ts +36 -0
- package/dist/core/objects/extract-with-retry.js +51 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.d.ts +1 -1
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.d.ts +28 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.js +61 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.d.ts +2 -1
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.js +16 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +22 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +20 -1
- package/dist/core/objects/index/index.diff.js +0 -1
- package/dist/core/objects/index/index.model.d.ts +2 -3
- package/dist/core/objects/index/index.model.js +17 -6
- package/dist/core/objects/materialized-view/changes/materialized-view.base.d.ts +1 -1
- package/dist/core/objects/materialized-view/changes/materialized-view.security-label.d.ts +28 -0
- package/dist/core/objects/materialized-view/changes/materialized-view.security-label.js +61 -0
- package/dist/core/objects/materialized-view/changes/materialized-view.types.d.ts +2 -1
- package/dist/core/objects/materialized-view/materialized-view.diff.js +18 -0
- package/dist/core/objects/materialized-view/materialized-view.model.d.ts +24 -1
- package/dist/core/objects/materialized-view/materialized-view.model.js +40 -5
- package/dist/core/objects/procedure/changes/procedure.base.d.ts +1 -1
- package/dist/core/objects/procedure/changes/procedure.security-label.d.ts +28 -0
- package/dist/core/objects/procedure/changes/procedure.security-label.js +69 -0
- package/dist/core/objects/procedure/changes/procedure.types.d.ts +2 -1
- package/dist/core/objects/procedure/procedure.diff.js +16 -0
- package/dist/core/objects/procedure/procedure.model.d.ts +12 -1
- package/dist/core/objects/procedure/procedure.model.js +39 -5
- package/dist/core/objects/publication/changes/publication.base.d.ts +1 -1
- package/dist/core/objects/publication/changes/publication.security-label.d.ts +28 -0
- package/dist/core/objects/publication/changes/publication.security-label.js +61 -0
- package/dist/core/objects/publication/changes/publication.types.d.ts +2 -1
- package/dist/core/objects/publication/publication.diff.js +16 -0
- package/dist/core/objects/publication/publication.model.d.ts +14 -0
- package/dist/core/objects/publication/publication.model.js +20 -1
- package/dist/core/objects/rls-policy/rls-policy.diff.js +13 -1
- package/dist/core/objects/role/changes/role.base.d.ts +1 -1
- package/dist/core/objects/role/changes/role.security-label.d.ts +28 -0
- package/dist/core/objects/role/changes/role.security-label.js +61 -0
- package/dist/core/objects/role/changes/role.types.d.ts +2 -1
- package/dist/core/objects/role/role.diff.js +16 -0
- package/dist/core/objects/role/role.model.d.ts +10 -0
- package/dist/core/objects/role/role.model.js +29 -0
- package/dist/core/objects/rule/rule.model.d.ts +2 -1
- package/dist/core/objects/rule/rule.model.js +20 -3
- package/dist/core/objects/schema/changes/schema.base.d.ts +1 -1
- package/dist/core/objects/schema/changes/schema.security-label.d.ts +28 -0
- package/dist/core/objects/schema/changes/schema.security-label.js +61 -0
- package/dist/core/objects/schema/changes/schema.types.d.ts +2 -1
- package/dist/core/objects/schema/schema.diff.js +24 -1
- package/dist/core/objects/schema/schema.model.d.ts +10 -0
- package/dist/core/objects/schema/schema.model.js +18 -1
- package/dist/core/objects/security-label.types.d.ts +20 -0
- package/dist/core/objects/security-label.types.js +46 -0
- package/dist/core/objects/sequence/changes/sequence.base.d.ts +1 -1
- package/dist/core/objects/sequence/changes/sequence.security-label.d.ts +28 -0
- package/dist/core/objects/sequence/changes/sequence.security-label.js +61 -0
- package/dist/core/objects/sequence/changes/sequence.types.d.ts +2 -1
- package/dist/core/objects/sequence/sequence.diff.d.ts +2 -1
- package/dist/core/objects/sequence/sequence.diff.js +44 -4
- package/dist/core/objects/sequence/sequence.model.d.ts +10 -0
- package/dist/core/objects/sequence/sequence.model.js +19 -1
- package/dist/core/objects/subscription/changes/subscription.base.d.ts +1 -1
- package/dist/core/objects/subscription/changes/subscription.security-label.d.ts +28 -0
- package/dist/core/objects/subscription/changes/subscription.security-label.js +61 -0
- package/dist/core/objects/subscription/changes/subscription.types.d.ts +2 -1
- package/dist/core/objects/subscription/subscription.diff.js +16 -0
- package/dist/core/objects/subscription/subscription.model.d.ts +10 -0
- package/dist/core/objects/subscription/subscription.model.js +19 -1
- package/dist/core/objects/table/changes/table.alter.d.ts +12 -1
- package/dist/core/objects/table/changes/table.alter.js +20 -2
- package/dist/core/objects/table/changes/table.base.d.ts +1 -1
- package/dist/core/objects/table/changes/table.security-label.d.ts +63 -0
- package/dist/core/objects/table/changes/table.security-label.js +134 -0
- package/dist/core/objects/table/changes/table.types.d.ts +2 -1
- package/dist/core/objects/table/table.diff.js +68 -15
- package/dist/core/objects/table/table.model.d.ts +36 -1
- package/dist/core/objects/table/table.model.js +74 -7
- package/dist/core/objects/trigger/trigger.model.d.ts +2 -1
- package/dist/core/objects/trigger/trigger.model.js +20 -4
- package/dist/core/objects/type/composite-type/changes/composite-type.base.d.ts +1 -1
- package/dist/core/objects/type/composite-type/changes/composite-type.security-label.d.ts +28 -0
- package/dist/core/objects/type/composite-type/changes/composite-type.security-label.js +61 -0
- package/dist/core/objects/type/composite-type/changes/composite-type.types.d.ts +2 -1
- package/dist/core/objects/type/composite-type/composite-type.diff.js +16 -0
- package/dist/core/objects/type/composite-type/composite-type.model.d.ts +22 -0
- package/dist/core/objects/type/composite-type/composite-type.model.js +22 -2
- package/dist/core/objects/type/enum/changes/enum.base.d.ts +1 -1
- package/dist/core/objects/type/enum/changes/enum.security-label.d.ts +28 -0
- package/dist/core/objects/type/enum/changes/enum.security-label.js +61 -0
- package/dist/core/objects/type/enum/changes/enum.types.d.ts +2 -1
- package/dist/core/objects/type/enum/enum.diff.js +16 -0
- package/dist/core/objects/type/enum/enum.model.d.ts +10 -0
- package/dist/core/objects/type/enum/enum.model.js +20 -1
- package/dist/core/objects/type/range/changes/range.base.d.ts +1 -1
- package/dist/core/objects/type/range/changes/range.security-label.d.ts +28 -0
- package/dist/core/objects/type/range/changes/range.security-label.js +61 -0
- package/dist/core/objects/type/range/changes/range.types.d.ts +2 -1
- package/dist/core/objects/type/range/range.diff.js +16 -0
- package/dist/core/objects/type/range/range.model.d.ts +10 -0
- package/dist/core/objects/type/range/range.model.js +19 -1
- package/dist/core/objects/utils.d.ts +2 -0
- package/dist/core/objects/utils.js +6 -0
- package/dist/core/objects/view/changes/view.base.d.ts +1 -1
- package/dist/core/objects/view/changes/view.security-label.d.ts +28 -0
- package/dist/core/objects/view/changes/view.security-label.js +61 -0
- package/dist/core/objects/view/changes/view.types.d.ts +2 -1
- package/dist/core/objects/view/view.diff.js +13 -0
- package/dist/core/objects/view/view.model.d.ts +28 -1
- package/dist/core/objects/view/view.model.js +40 -5
- package/dist/core/plan/create.js +3 -1
- package/dist/core/plan/sql-format/fixtures.js +1 -0
- package/dist/core/plan/types.d.ts +8 -0
- package/dist/core/{post-diff-cycle-breaking.d.ts → post-diff-normalization.d.ts} +8 -1
- package/dist/core/post-diff-normalization.js +202 -0
- package/dist/core/sort/cycle-breakers.js +1 -1
- package/dist/core/sort/utils.d.ts +10 -0
- package/dist/core/sort/utils.js +28 -0
- package/package.json +1 -1
- package/src/core/catalog.diff.ts +4 -2
- package/src/core/catalog.model.ts +21 -8
- package/src/core/expand-replace-dependencies.test.ts +131 -0
- package/src/core/expand-replace-dependencies.ts +24 -0
- package/src/core/integrations/filter/dsl.test.ts +27 -0
- package/src/core/integrations/filter/flatten.ts +16 -0
- package/src/core/objects/aggregate/aggregate.diff.ts +33 -0
- package/src/core/objects/aggregate/aggregate.model.ts +22 -1
- package/src/core/objects/aggregate/changes/aggregate.base.ts +5 -1
- package/src/core/objects/aggregate/changes/aggregate.security-label.ts +99 -0
- package/src/core/objects/aggregate/changes/aggregate.types.ts +3 -1
- package/src/core/objects/base.model.ts +2 -0
- package/src/core/objects/domain/changes/domain.base.ts +5 -1
- package/src/core/objects/domain/changes/domain.security-label.test.ts +56 -0
- package/src/core/objects/domain/changes/domain.security-label.ts +77 -0
- package/src/core/objects/domain/changes/domain.types.ts +3 -1
- package/src/core/objects/domain/domain.diff.ts +33 -0
- package/src/core/objects/domain/domain.model.ts +22 -1
- package/src/core/objects/event-trigger/changes/event-trigger.base.ts +1 -1
- package/src/core/objects/event-trigger/changes/event-trigger.security-label.ts +95 -0
- package/src/core/objects/event-trigger/changes/event-trigger.types.ts +3 -1
- package/src/core/objects/event-trigger/event-trigger.diff.ts +33 -0
- package/src/core/objects/event-trigger/event-trigger.model.ts +22 -1
- package/src/core/objects/extract-with-retry.test.ts +143 -0
- package/src/core/objects/extract-with-retry.ts +87 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +5 -1
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.ts +95 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.ts +3 -1
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.ts +33 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +24 -1
- package/src/core/objects/index/index.diff.ts +0 -1
- package/src/core/objects/index/index.model.test.ts +37 -1
- package/src/core/objects/index/index.model.ts +25 -6
- package/src/core/objects/materialized-view/changes/materialized-view.base.ts +5 -1
- package/src/core/objects/materialized-view/changes/materialized-view.security-label.test.ts +63 -0
- package/src/core/objects/materialized-view/changes/materialized-view.security-label.ts +95 -0
- package/src/core/objects/materialized-view/changes/materialized-view.types.ts +3 -1
- package/src/core/objects/materialized-view/materialized-view.diff.ts +37 -0
- package/src/core/objects/materialized-view/materialized-view.model.test.ts +93 -0
- package/src/core/objects/materialized-view/materialized-view.model.ts +52 -8
- package/src/core/objects/procedure/changes/procedure.base.ts +5 -1
- package/src/core/objects/procedure/changes/procedure.security-label.ts +105 -0
- package/src/core/objects/procedure/changes/procedure.types.ts +3 -1
- package/src/core/objects/procedure/procedure.diff.ts +33 -0
- package/src/core/objects/procedure/procedure.model.test.ts +117 -0
- package/src/core/objects/procedure/procedure.model.ts +51 -7
- package/src/core/objects/publication/changes/publication.base.ts +1 -1
- package/src/core/objects/publication/changes/publication.security-label.ts +95 -0
- package/src/core/objects/publication/changes/publication.types.ts +3 -1
- package/src/core/objects/publication/publication.diff.ts +33 -0
- package/src/core/objects/publication/publication.model.ts +24 -1
- package/src/core/objects/rls-policy/rls-policy.diff.ts +19 -1
- package/src/core/objects/role/changes/role.base.ts +2 -1
- package/src/core/objects/role/changes/role.security-label.ts +77 -0
- package/src/core/objects/role/changes/role.types.ts +3 -1
- package/src/core/objects/role/role.diff.ts +33 -0
- package/src/core/objects/role/role.model.ts +32 -0
- package/src/core/objects/rule/rule.model.test.ts +99 -0
- package/src/core/objects/rule/rule.model.ts +28 -4
- package/src/core/objects/schema/changes/schema.alter.test.ts +1 -0
- package/src/core/objects/schema/changes/schema.base.ts +5 -1
- package/src/core/objects/schema/changes/schema.create.test.ts +1 -0
- package/src/core/objects/schema/changes/schema.drop.test.ts +1 -0
- package/src/core/objects/schema/changes/schema.security-label.test.ts +76 -0
- package/src/core/objects/schema/changes/schema.security-label.ts +77 -0
- package/src/core/objects/schema/changes/schema.types.ts +3 -1
- package/src/core/objects/schema/schema.diff.test.ts +1 -0
- package/src/core/objects/schema/schema.diff.ts +43 -1
- package/src/core/objects/schema/schema.model.ts +21 -1
- package/src/core/objects/security-label.types.test.ts +106 -0
- package/src/core/objects/security-label.types.ts +61 -0
- package/src/core/objects/sequence/changes/sequence.base.ts +5 -1
- package/src/core/objects/sequence/changes/sequence.security-label.test.ts +58 -0
- package/src/core/objects/sequence/changes/sequence.security-label.ts +92 -0
- package/src/core/objects/sequence/changes/sequence.types.ts +3 -1
- package/src/core/objects/sequence/sequence.diff.test.ts +87 -0
- package/src/core/objects/sequence/sequence.diff.ts +64 -6
- package/src/core/objects/sequence/sequence.model.ts +22 -1
- package/src/core/objects/subscription/changes/subscription.base.ts +1 -1
- package/src/core/objects/subscription/changes/subscription.security-label.ts +95 -0
- package/src/core/objects/subscription/changes/subscription.types.ts +3 -1
- package/src/core/objects/subscription/subscription.diff.ts +33 -0
- package/src/core/objects/subscription/subscription.model.ts +22 -1
- package/src/core/objects/table/changes/table.alter.test.ts +13 -21
- package/src/core/objects/table/changes/table.alter.ts +30 -3
- package/src/core/objects/table/changes/table.base.ts +5 -1
- package/src/core/objects/table/changes/table.security-label.test.ts +140 -0
- package/src/core/objects/table/changes/table.security-label.ts +183 -0
- package/src/core/objects/table/changes/table.types.ts +3 -1
- package/src/core/objects/table/table.diff.ts +111 -19
- package/src/core/objects/table/table.model.test.ts +209 -0
- package/src/core/objects/table/table.model.ts +94 -9
- package/src/core/objects/trigger/trigger.model.test.ts +113 -0
- package/src/core/objects/trigger/trigger.model.ts +28 -5
- package/src/core/objects/type/composite-type/changes/composite-type.base.ts +5 -1
- package/src/core/objects/type/composite-type/changes/composite-type.security-label.ts +95 -0
- package/src/core/objects/type/composite-type/changes/composite-type.types.ts +3 -1
- package/src/core/objects/type/composite-type/composite-type.diff.ts +33 -0
- package/src/core/objects/type/composite-type/composite-type.model.ts +26 -2
- package/src/core/objects/type/enum/changes/enum.base.ts +5 -1
- package/src/core/objects/type/enum/changes/enum.security-label.ts +77 -0
- package/src/core/objects/type/enum/changes/enum.types.ts +3 -1
- package/src/core/objects/type/enum/enum.diff.ts +33 -0
- package/src/core/objects/type/enum/enum.model.ts +25 -1
- package/src/core/objects/type/range/changes/range.base.ts +5 -1
- package/src/core/objects/type/range/changes/range.security-label.ts +77 -0
- package/src/core/objects/type/range/changes/range.types.ts +3 -1
- package/src/core/objects/type/range/range.diff.ts +33 -0
- package/src/core/objects/type/range/range.model.ts +22 -1
- package/src/core/objects/utils.ts +6 -0
- package/src/core/objects/view/changes/view.base.ts +5 -1
- package/src/core/objects/view/changes/view.security-label.test.ts +64 -0
- package/src/core/objects/view/changes/view.security-label.ts +77 -0
- package/src/core/objects/view/changes/view.types.ts +3 -1
- package/src/core/objects/view/view.diff.ts +31 -0
- package/src/core/objects/view/view.model.test.ts +90 -0
- package/src/core/objects/view/view.model.ts +53 -7
- package/src/core/plan/create.ts +3 -1
- package/src/core/plan/sql-format/fixtures.ts +1 -0
- package/src/core/plan/types.ts +8 -0
- package/src/core/{post-diff-cycle-breaking.test.ts → post-diff-normalization.test.ts} +168 -4
- package/src/core/post-diff-normalization.ts +260 -0
- package/src/core/sort/cycle-breakers.ts +1 -1
- package/src/core/sort/utils.ts +38 -0
- package/dist/core/post-diff-cycle-breaking.js +0 -100
- package/src/core/post-diff-cycle-breaking.ts +0 -138
|
@@ -2,6 +2,10 @@ import { sql } from "@ts-safeql/sql-tag";
|
|
|
2
2
|
import type { Pool } from "pg";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { BasePgModel } from "../base.model.ts";
|
|
5
|
+
import {
|
|
6
|
+
type SecurityLabelProps,
|
|
7
|
+
securityLabelPropsSchema,
|
|
8
|
+
} from "../security-label.types.ts";
|
|
5
9
|
|
|
6
10
|
const EventTriggerEnabledSchema = z.enum([
|
|
7
11
|
"O", // ORIGIN - trigger fires in origin mode
|
|
@@ -19,6 +23,7 @@ const eventTriggerPropsSchema = z.object({
|
|
|
19
23
|
tags: z.array(z.string()).nullable(),
|
|
20
24
|
owner: z.string(),
|
|
21
25
|
comment: z.string().nullable(),
|
|
26
|
+
security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
export type EventTriggerProps = z.infer<typeof eventTriggerPropsSchema>;
|
|
@@ -32,6 +37,7 @@ export class EventTrigger extends BasePgModel {
|
|
|
32
37
|
public readonly tags: EventTriggerProps["tags"];
|
|
33
38
|
public readonly owner: EventTriggerProps["owner"];
|
|
34
39
|
public readonly comment: EventTriggerProps["comment"];
|
|
40
|
+
public readonly security_labels: SecurityLabelProps[];
|
|
35
41
|
|
|
36
42
|
constructor(props: EventTriggerProps) {
|
|
37
43
|
super();
|
|
@@ -47,6 +53,7 @@ export class EventTrigger extends BasePgModel {
|
|
|
47
53
|
this.tags = props.tags;
|
|
48
54
|
this.owner = props.owner;
|
|
49
55
|
this.comment = props.comment;
|
|
56
|
+
this.security_labels = props.security_labels ?? [];
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
get stableId(): `eventTrigger:${string}` {
|
|
@@ -68,6 +75,7 @@ export class EventTrigger extends BasePgModel {
|
|
|
68
75
|
tags: this.tags,
|
|
69
76
|
owner: this.owner,
|
|
70
77
|
comment: this.comment,
|
|
78
|
+
security_labels: this.security_labels,
|
|
71
79
|
};
|
|
72
80
|
}
|
|
73
81
|
}
|
|
@@ -90,7 +98,20 @@ select
|
|
|
90
98
|
et.evtenabled as enabled,
|
|
91
99
|
et.evttags as tags,
|
|
92
100
|
et.evtowner::regrole::text as owner,
|
|
93
|
-
obj_description(et.oid, 'pg_event_trigger') as comment
|
|
101
|
+
obj_description(et.oid, 'pg_event_trigger') as comment,
|
|
102
|
+
coalesce(
|
|
103
|
+
(
|
|
104
|
+
select json_agg(
|
|
105
|
+
json_build_object('provider', sl.provider, 'label', sl.label)
|
|
106
|
+
order by sl.provider
|
|
107
|
+
)
|
|
108
|
+
from pg_catalog.pg_seclabel sl
|
|
109
|
+
where sl.objoid = et.oid
|
|
110
|
+
and sl.classoid = 'pg_event_trigger'::regclass
|
|
111
|
+
and sl.objsubid = 0
|
|
112
|
+
),
|
|
113
|
+
'[]'::json
|
|
114
|
+
) as security_labels
|
|
94
115
|
from pg_catalog.pg_event_trigger et
|
|
95
116
|
join pg_catalog.pg_proc p on p.oid = et.evtfoid
|
|
96
117
|
left join extension_oids e on e.objid = et.oid
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
extractWithDefinitionRetry,
|
|
4
|
+
resolveExtractRetries,
|
|
5
|
+
} from "./extract-with-retry.ts";
|
|
6
|
+
|
|
7
|
+
type Row = { id: string; definition: string | null };
|
|
8
|
+
|
|
9
|
+
const hasNullDefinition = (r: Row) => r.definition === null;
|
|
10
|
+
|
|
11
|
+
describe("resolveExtractRetries", () => {
|
|
12
|
+
const originalEnv = process.env.PGDELTA_EXTRACT_RETRIES;
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (originalEnv === undefined) {
|
|
15
|
+
process.env.PGDELTA_EXTRACT_RETRIES = undefined;
|
|
16
|
+
delete process.env.PGDELTA_EXTRACT_RETRIES;
|
|
17
|
+
} else {
|
|
18
|
+
process.env.PGDELTA_EXTRACT_RETRIES = originalEnv;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("defaults to 1 when option and env are unset", () => {
|
|
23
|
+
delete process.env.PGDELTA_EXTRACT_RETRIES;
|
|
24
|
+
expect(resolveExtractRetries()).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("uses option when provided", () => {
|
|
28
|
+
process.env.PGDELTA_EXTRACT_RETRIES = "5";
|
|
29
|
+
expect(resolveExtractRetries(0)).toBe(0);
|
|
30
|
+
expect(resolveExtractRetries(1)).toBe(1);
|
|
31
|
+
expect(resolveExtractRetries(7)).toBe(7);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("falls back to env when option is undefined", () => {
|
|
35
|
+
process.env.PGDELTA_EXTRACT_RETRIES = "4";
|
|
36
|
+
expect(resolveExtractRetries()).toBe(4);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("clamps negative values to 0", () => {
|
|
40
|
+
delete process.env.PGDELTA_EXTRACT_RETRIES;
|
|
41
|
+
expect(resolveExtractRetries(-3)).toBe(0);
|
|
42
|
+
process.env.PGDELTA_EXTRACT_RETRIES = "-9";
|
|
43
|
+
expect(resolveExtractRetries()).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("ignores non-numeric env values", () => {
|
|
47
|
+
process.env.PGDELTA_EXTRACT_RETRIES = "not-a-number";
|
|
48
|
+
expect(resolveExtractRetries()).toBe(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("ignores empty env string", () => {
|
|
52
|
+
process.env.PGDELTA_EXTRACT_RETRIES = "";
|
|
53
|
+
expect(resolveExtractRetries()).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("extractWithDefinitionRetry", () => {
|
|
58
|
+
test("returns first attempt when no row has null definition", async () => {
|
|
59
|
+
let attempts = 0;
|
|
60
|
+
const rows = await extractWithDefinitionRetry<Row>({
|
|
61
|
+
label: "test",
|
|
62
|
+
query: async () => {
|
|
63
|
+
attempts++;
|
|
64
|
+
return [{ id: "a", definition: "OK" }];
|
|
65
|
+
},
|
|
66
|
+
hasNullDefinition,
|
|
67
|
+
options: { retries: 2, backoffMs: 0 },
|
|
68
|
+
});
|
|
69
|
+
expect(attempts).toBe(1);
|
|
70
|
+
expect(rows).toEqual([{ id: "a", definition: "OK" }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("retries when definition is null and succeeds on attempt 2", async () => {
|
|
74
|
+
let attempts = 0;
|
|
75
|
+
const rows = await extractWithDefinitionRetry<Row>({
|
|
76
|
+
label: "test",
|
|
77
|
+
query: async () => {
|
|
78
|
+
attempts++;
|
|
79
|
+
if (attempts === 1) {
|
|
80
|
+
return [
|
|
81
|
+
{ id: "a", definition: "OK" },
|
|
82
|
+
{ id: "b", definition: null },
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
return [{ id: "a", definition: "OK" }];
|
|
86
|
+
},
|
|
87
|
+
hasNullDefinition,
|
|
88
|
+
options: { retries: 2, backoffMs: 0 },
|
|
89
|
+
});
|
|
90
|
+
expect(attempts).toBe(2);
|
|
91
|
+
expect(rows).toEqual([{ id: "a", definition: "OK" }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns last-attempt rows (with offenders) once retries are exhausted", async () => {
|
|
95
|
+
let attempts = 0;
|
|
96
|
+
const rows = await extractWithDefinitionRetry<Row>({
|
|
97
|
+
label: "test",
|
|
98
|
+
query: async () => {
|
|
99
|
+
attempts++;
|
|
100
|
+
return [
|
|
101
|
+
{ id: "a", definition: "OK" },
|
|
102
|
+
{ id: "b", definition: null },
|
|
103
|
+
];
|
|
104
|
+
},
|
|
105
|
+
hasNullDefinition,
|
|
106
|
+
options: { retries: 2, backoffMs: 0 },
|
|
107
|
+
});
|
|
108
|
+
expect(attempts).toBe(3);
|
|
109
|
+
expect(rows).toEqual([
|
|
110
|
+
{ id: "a", definition: "OK" },
|
|
111
|
+
{ id: "b", definition: null },
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("retries: 0 disables retrying entirely", async () => {
|
|
116
|
+
let attempts = 0;
|
|
117
|
+
const rows = await extractWithDefinitionRetry<Row>({
|
|
118
|
+
label: "test",
|
|
119
|
+
query: async () => {
|
|
120
|
+
attempts++;
|
|
121
|
+
return [{ id: "b", definition: null }];
|
|
122
|
+
},
|
|
123
|
+
hasNullDefinition,
|
|
124
|
+
options: { retries: 0, backoffMs: 0 },
|
|
125
|
+
});
|
|
126
|
+
expect(attempts).toBe(1);
|
|
127
|
+
expect(rows).toEqual([{ id: "b", definition: null }]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("retries: 5 attempts up to 6 times before giving up", async () => {
|
|
131
|
+
let attempts = 0;
|
|
132
|
+
await extractWithDefinitionRetry<Row>({
|
|
133
|
+
label: "test",
|
|
134
|
+
query: async () => {
|
|
135
|
+
attempts++;
|
|
136
|
+
return [{ id: "b", definition: null }];
|
|
137
|
+
},
|
|
138
|
+
hasNullDefinition,
|
|
139
|
+
options: { retries: 5, backoffMs: 0 },
|
|
140
|
+
});
|
|
141
|
+
expect(attempts).toBe(6);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
|
|
3
|
+
const log = debug("pg-delta:extract");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_RETRIES = 1;
|
|
6
|
+
const DEFAULT_BACKOFF_MS = 50;
|
|
7
|
+
|
|
8
|
+
export interface ExtractRetryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Number of retry attempts to make when a `pg_get_*def()` call returns NULL
|
|
11
|
+
* for at least one row. Total attempts is `retries + 1`. Negative values are
|
|
12
|
+
* clamped to 0. When this option is undefined the value is read from the
|
|
13
|
+
* `PGDELTA_EXTRACT_RETRIES` environment variable, falling back to a default
|
|
14
|
+
* of 1 (i.e. the first attempt plus one retry, 2 attempts total).
|
|
15
|
+
*/
|
|
16
|
+
retries?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Delay between retry attempts in milliseconds; the actual wait is
|
|
19
|
+
* `backoffMs * attemptNumber` (linear). Defaults to 50. Set to 0 in tests.
|
|
20
|
+
*/
|
|
21
|
+
backoffMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveExtractRetries(option?: number): number {
|
|
25
|
+
if (typeof option === "number" && Number.isFinite(option)) {
|
|
26
|
+
return Math.max(0, Math.floor(option));
|
|
27
|
+
}
|
|
28
|
+
const envVal = process.env.PGDELTA_EXTRACT_RETRIES;
|
|
29
|
+
if (envVal !== undefined && envVal !== "") {
|
|
30
|
+
const n = Number(envVal);
|
|
31
|
+
if (Number.isFinite(n)) return Math.max(0, Math.floor(n));
|
|
32
|
+
}
|
|
33
|
+
return DEFAULT_RETRIES;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sleep = (ms: number) =>
|
|
37
|
+
ms > 0 ? new Promise<void>((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs `query()` up to `retries + 1` times, retrying as long as at least one
|
|
41
|
+
* row in the result satisfies `hasNullDefinition`. The retry exists because
|
|
42
|
+
* `pg_get_<x>def()` can return NULL transiently when the underlying catalog
|
|
43
|
+
* row is dropped concurrently or the catalog state is in flux; in practice a
|
|
44
|
+
* second attempt either no longer sees the dropped row or succeeds in
|
|
45
|
+
* resolving the definition.
|
|
46
|
+
*
|
|
47
|
+
* Returns the rows from the first attempt with no offenders, or — once
|
|
48
|
+
* retries are exhausted — the rows from the final attempt (still containing
|
|
49
|
+
* offenders). The caller is responsible for the final filter so this helper
|
|
50
|
+
* works for both flat schemas (definition on the row) and nested schemas
|
|
51
|
+
* (definition on a child collection, e.g. table constraints).
|
|
52
|
+
*/
|
|
53
|
+
export async function extractWithDefinitionRetry<TRow>(params: {
|
|
54
|
+
label: string;
|
|
55
|
+
query: () => Promise<TRow[]>;
|
|
56
|
+
hasNullDefinition: (row: TRow) => boolean;
|
|
57
|
+
options?: ExtractRetryOptions;
|
|
58
|
+
}): Promise<TRow[]> {
|
|
59
|
+
const retries = resolveExtractRetries(params.options?.retries);
|
|
60
|
+
const backoffMs = params.options?.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
61
|
+
const maxAttempts = retries + 1;
|
|
62
|
+
|
|
63
|
+
let rows: TRow[] = [];
|
|
64
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
65
|
+
rows = await params.query();
|
|
66
|
+
const offenders = rows.filter(params.hasNullDefinition).length;
|
|
67
|
+
if (offenders === 0) return rows;
|
|
68
|
+
if (attempt < maxAttempts) {
|
|
69
|
+
log(
|
|
70
|
+
"%s: pg_get_*def() returned NULL for %d row(s) on attempt %d/%d; retrying",
|
|
71
|
+
params.label,
|
|
72
|
+
offenders,
|
|
73
|
+
attempt,
|
|
74
|
+
maxAttempts,
|
|
75
|
+
);
|
|
76
|
+
await sleep(backoffMs * attempt);
|
|
77
|
+
} else {
|
|
78
|
+
log(
|
|
79
|
+
"%s: pg_get_*def() returned NULL for %d row(s) after %d attempt(s); skipping",
|
|
80
|
+
params.label,
|
|
81
|
+
offenders,
|
|
82
|
+
maxAttempts,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return rows;
|
|
87
|
+
}
|
|
@@ -3,7 +3,11 @@ import type { ForeignTable } from "../foreign-table.model.ts";
|
|
|
3
3
|
|
|
4
4
|
abstract class BaseForeignTableChange extends BaseChange {
|
|
5
5
|
abstract readonly foreignTable: ForeignTable;
|
|
6
|
-
abstract readonly scope:
|
|
6
|
+
abstract readonly scope:
|
|
7
|
+
| "object"
|
|
8
|
+
| "comment"
|
|
9
|
+
| "privilege"
|
|
10
|
+
| "security_label";
|
|
7
11
|
readonly objectType: "foreign_table" = "foreign_table";
|
|
8
12
|
}
|
|
9
13
|
|
package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { quoteLiteral } from "../../../base.change.ts";
|
|
2
|
+
import type { SecurityLabelProps } from "../../../security-label.types.ts";
|
|
3
|
+
import { stableId } from "../../../utils.ts";
|
|
4
|
+
import type { ForeignTable } from "../foreign-table.model.ts";
|
|
5
|
+
import {
|
|
6
|
+
CreateForeignTableChange,
|
|
7
|
+
DropForeignTableChange,
|
|
8
|
+
} from "./foreign-table.base.ts";
|
|
9
|
+
|
|
10
|
+
export type SecurityLabelForeignTable =
|
|
11
|
+
| CreateSecurityLabelOnForeignTable
|
|
12
|
+
| DropSecurityLabelOnForeignTable;
|
|
13
|
+
|
|
14
|
+
export class CreateSecurityLabelOnForeignTable extends CreateForeignTableChange {
|
|
15
|
+
public readonly foreignTable: ForeignTable;
|
|
16
|
+
public readonly securityLabel: SecurityLabelProps;
|
|
17
|
+
public readonly scope = "security_label" as const;
|
|
18
|
+
|
|
19
|
+
constructor(props: {
|
|
20
|
+
foreignTable: ForeignTable;
|
|
21
|
+
securityLabel: SecurityLabelProps;
|
|
22
|
+
}) {
|
|
23
|
+
super();
|
|
24
|
+
this.foreignTable = props.foreignTable;
|
|
25
|
+
this.securityLabel = props.securityLabel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get creates() {
|
|
29
|
+
return [
|
|
30
|
+
stableId.securityLabel(
|
|
31
|
+
this.foreignTable.stableId,
|
|
32
|
+
this.securityLabel.provider,
|
|
33
|
+
),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get requires() {
|
|
38
|
+
return [this.foreignTable.stableId];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
serialize(): string {
|
|
42
|
+
return [
|
|
43
|
+
"SECURITY LABEL FOR",
|
|
44
|
+
this.securityLabel.provider,
|
|
45
|
+
"ON FOREIGN TABLE",
|
|
46
|
+
`${this.foreignTable.schema}.${this.foreignTable.name}`,
|
|
47
|
+
"IS",
|
|
48
|
+
quoteLiteral(this.securityLabel.label),
|
|
49
|
+
].join(" ");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class DropSecurityLabelOnForeignTable extends DropForeignTableChange {
|
|
54
|
+
public readonly foreignTable: ForeignTable;
|
|
55
|
+
public readonly securityLabel: SecurityLabelProps;
|
|
56
|
+
public readonly scope = "security_label" as const;
|
|
57
|
+
|
|
58
|
+
constructor(props: {
|
|
59
|
+
foreignTable: ForeignTable;
|
|
60
|
+
securityLabel: SecurityLabelProps;
|
|
61
|
+
}) {
|
|
62
|
+
super();
|
|
63
|
+
this.foreignTable = props.foreignTable;
|
|
64
|
+
this.securityLabel = props.securityLabel;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get drops() {
|
|
68
|
+
return [
|
|
69
|
+
stableId.securityLabel(
|
|
70
|
+
this.foreignTable.stableId,
|
|
71
|
+
this.securityLabel.provider,
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get requires() {
|
|
77
|
+
return [
|
|
78
|
+
stableId.securityLabel(
|
|
79
|
+
this.foreignTable.stableId,
|
|
80
|
+
this.securityLabel.provider,
|
|
81
|
+
),
|
|
82
|
+
this.foreignTable.stableId,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
serialize(): string {
|
|
87
|
+
return [
|
|
88
|
+
"SECURITY LABEL FOR",
|
|
89
|
+
this.securityLabel.provider,
|
|
90
|
+
"ON FOREIGN TABLE",
|
|
91
|
+
`${this.foreignTable.schema}.${this.foreignTable.name}`,
|
|
92
|
+
"IS NULL",
|
|
93
|
+
].join(" ");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { CommentForeignTable } from "./foreign-table.comment.ts";
|
|
|
3
3
|
import type { CreateForeignTable } from "./foreign-table.create.ts";
|
|
4
4
|
import type { DropForeignTable } from "./foreign-table.drop.ts";
|
|
5
5
|
import type { ForeignTablePrivilege } from "./foreign-table.privilege.ts";
|
|
6
|
+
import type { SecurityLabelForeignTable } from "./foreign-table.security-label.ts";
|
|
6
7
|
|
|
7
8
|
/** Union of all foreign-table-related change variants (`objectType: "foreign_table"`). @category Change Types */
|
|
8
9
|
export type ForeignTableChange =
|
|
@@ -10,4 +11,5 @@ export type ForeignTableChange =
|
|
|
10
11
|
| CommentForeignTable
|
|
11
12
|
| CreateForeignTable
|
|
12
13
|
| DropForeignTable
|
|
13
|
-
| ForeignTablePrivilege
|
|
14
|
+
| ForeignTablePrivilege
|
|
15
|
+
| SecurityLabelForeignTable;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
filterPublicBuiltInDefaults,
|
|
6
6
|
} from "../../base.privilege-diff.ts";
|
|
7
7
|
import type { ObjectDiffContext } from "../../diff-context.ts";
|
|
8
|
+
import { diffSecurityLabels } from "../../security-label.types.ts";
|
|
8
9
|
import {
|
|
9
10
|
AlterForeignTableAddColumn,
|
|
10
11
|
AlterForeignTableAlterColumnDropDefault,
|
|
@@ -27,6 +28,10 @@ import {
|
|
|
27
28
|
RevokeForeignTablePrivileges,
|
|
28
29
|
RevokeGrantOptionForeignTablePrivileges,
|
|
29
30
|
} from "./changes/foreign-table.privilege.ts";
|
|
31
|
+
import {
|
|
32
|
+
CreateSecurityLabelOnForeignTable,
|
|
33
|
+
DropSecurityLabelOnForeignTable,
|
|
34
|
+
} from "./changes/foreign-table.security-label.ts";
|
|
30
35
|
import type { ForeignTableChange } from "./changes/foreign-table.types.ts";
|
|
31
36
|
import type { ForeignTable } from "./foreign-table.model.ts";
|
|
32
37
|
|
|
@@ -70,6 +75,14 @@ export function diffForeignTables(
|
|
|
70
75
|
new CreateCommentOnForeignTable({ foreignTable: createdTable }),
|
|
71
76
|
);
|
|
72
77
|
}
|
|
78
|
+
for (const label of createdTable.security_labels) {
|
|
79
|
+
changes.push(
|
|
80
|
+
new CreateSecurityLabelOnForeignTable({
|
|
81
|
+
foreignTable: createdTable,
|
|
82
|
+
securityLabel: label,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
73
86
|
|
|
74
87
|
// PRIVILEGES: For created objects, compare against default privileges state
|
|
75
88
|
const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(
|
|
@@ -249,6 +262,26 @@ export function diffForeignTables(
|
|
|
249
262
|
}
|
|
250
263
|
}
|
|
251
264
|
|
|
265
|
+
// SECURITY LABELS
|
|
266
|
+
changes.push(
|
|
267
|
+
...diffSecurityLabels<
|
|
268
|
+
CreateSecurityLabelOnForeignTable | DropSecurityLabelOnForeignTable
|
|
269
|
+
>(
|
|
270
|
+
mainTable.security_labels,
|
|
271
|
+
branchTable.security_labels,
|
|
272
|
+
(securityLabel) =>
|
|
273
|
+
new CreateSecurityLabelOnForeignTable({
|
|
274
|
+
foreignTable: branchTable,
|
|
275
|
+
securityLabel,
|
|
276
|
+
}),
|
|
277
|
+
(securityLabel) =>
|
|
278
|
+
new DropSecurityLabelOnForeignTable({
|
|
279
|
+
foreignTable: mainTable,
|
|
280
|
+
securityLabel,
|
|
281
|
+
}),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
|
|
252
285
|
// PRIVILEGES
|
|
253
286
|
const mainPrivilegesFiltered = filterPublicBuiltInDefaults(
|
|
254
287
|
"foreign_table",
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type PrivilegeProps,
|
|
11
11
|
privilegePropsSchema,
|
|
12
12
|
} from "../../base.privilege-diff.ts";
|
|
13
|
+
import {
|
|
14
|
+
normalizeSecurityLabels,
|
|
15
|
+
type SecurityLabelProps,
|
|
16
|
+
securityLabelPropsSchema,
|
|
17
|
+
} from "../../security-label.types.ts";
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* All properties exposed by CREATE FOREIGN TABLE statement are included in diff output.
|
|
@@ -30,6 +35,7 @@ const foreignTablePropsSchema = z.object({
|
|
|
30
35
|
comment: z.string().nullable(),
|
|
31
36
|
columns: z.array(columnPropsSchema),
|
|
32
37
|
privileges: z.array(privilegePropsSchema),
|
|
38
|
+
security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
type ForeignTablePrivilegeProps = PrivilegeProps;
|
|
@@ -44,6 +50,7 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
44
50
|
public readonly comment: ForeignTableProps["comment"];
|
|
45
51
|
public readonly columns: ForeignTableProps["columns"];
|
|
46
52
|
public readonly privileges: ForeignTablePrivilegeProps[];
|
|
53
|
+
public readonly security_labels: SecurityLabelProps[];
|
|
47
54
|
|
|
48
55
|
constructor(props: ForeignTableProps) {
|
|
49
56
|
super();
|
|
@@ -59,6 +66,7 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
59
66
|
this.comment = props.comment;
|
|
60
67
|
this.columns = props.columns;
|
|
61
68
|
this.privileges = props.privileges;
|
|
69
|
+
this.security_labels = props.security_labels ?? [];
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
get stableId(): `foreignTable:${string}` {
|
|
@@ -80,6 +88,7 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
80
88
|
comment: this.comment,
|
|
81
89
|
columns: this.columns,
|
|
82
90
|
privileges: this.privileges,
|
|
91
|
+
security_labels: this.security_labels,
|
|
83
92
|
};
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -104,6 +113,7 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
104
113
|
data: {
|
|
105
114
|
...this.dataFields,
|
|
106
115
|
columns: normalizeColumns(),
|
|
116
|
+
security_labels: normalizeSecurityLabels(this.security_labels),
|
|
107
117
|
},
|
|
108
118
|
};
|
|
109
119
|
}
|
|
@@ -209,7 +219,20 @@ export async function extractForeignTables(
|
|
|
209
219
|
join lateral aclexplode(src.acl) as x(grantor, grantee, privilege_type, is_grantable) on true
|
|
210
220
|
group by x.grantee, x.privilege_type
|
|
211
221
|
) as grp
|
|
212
|
-
), '[]') as privileges
|
|
222
|
+
), '[]') as privileges,
|
|
223
|
+
coalesce(
|
|
224
|
+
(
|
|
225
|
+
select json_agg(
|
|
226
|
+
json_build_object('provider', sl.provider, 'label', sl.label)
|
|
227
|
+
order by sl.provider
|
|
228
|
+
)
|
|
229
|
+
from pg_catalog.pg_seclabel sl
|
|
230
|
+
where sl.objoid = ft.oid
|
|
231
|
+
and sl.classoid = 'pg_class'::regclass
|
|
232
|
+
and sl.objsubid = 0
|
|
233
|
+
),
|
|
234
|
+
'[]'::json
|
|
235
|
+
) as security_labels
|
|
213
236
|
from
|
|
214
237
|
foreign_tables ft
|
|
215
238
|
left join pg_attribute a on a.attrelid = ft.oid and a.attnum > 0 and not a.attisdropped
|
|
@@ -36,8 +36,19 @@ const baseRow = {
|
|
|
36
36
|
const mockPool = (rows: unknown[]): Pool =>
|
|
37
37
|
({ query: async () => ({ rows }) }) as unknown as Pool;
|
|
38
38
|
|
|
39
|
+
const mockPoolSequence = (...attempts: unknown[][]): Pool => {
|
|
40
|
+
let i = 0;
|
|
41
|
+
return {
|
|
42
|
+
query: async () => ({
|
|
43
|
+
rows: attempts[Math.min(i++, attempts.length - 1)],
|
|
44
|
+
}),
|
|
45
|
+
} as unknown as Pool;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const NO_BACKOFF = { backoffMs: 0 } as const;
|
|
49
|
+
|
|
39
50
|
describe("extractIndexes", () => {
|
|
40
|
-
test("skips rows where pg_get_indexdef returned NULL", async () => {
|
|
51
|
+
test("skips rows where pg_get_indexdef returned NULL after exhausting retries", async () => {
|
|
41
52
|
const indexes = await extractIndexes(
|
|
42
53
|
mockPool([
|
|
43
54
|
{
|
|
@@ -47,6 +58,7 @@ describe("extractIndexes", () => {
|
|
|
47
58
|
},
|
|
48
59
|
{ ...baseRow, name: '"orphan_idx"', definition: null },
|
|
49
60
|
]),
|
|
61
|
+
NO_BACKOFF,
|
|
50
62
|
);
|
|
51
63
|
|
|
52
64
|
expect(indexes).toHaveLength(1);
|
|
@@ -59,6 +71,7 @@ describe("extractIndexes", () => {
|
|
|
59
71
|
await expect(
|
|
60
72
|
extractIndexes(
|
|
61
73
|
mockPool([{ ...baseRow, name: '"orphan"', definition: null }]),
|
|
74
|
+
NO_BACKOFF,
|
|
62
75
|
),
|
|
63
76
|
).resolves.toEqual([]);
|
|
64
77
|
});
|
|
@@ -77,7 +90,30 @@ describe("extractIndexes", () => {
|
|
|
77
90
|
definition: "CREATE INDEX b ON users (id)",
|
|
78
91
|
},
|
|
79
92
|
]),
|
|
93
|
+
NO_BACKOFF,
|
|
80
94
|
);
|
|
81
95
|
expect(indexes.map((i) => i.name)).toEqual(['"a"', '"b"']);
|
|
82
96
|
});
|
|
97
|
+
|
|
98
|
+
test("recovers when pg_get_indexdef is NULL on first attempt but resolved on retry", async () => {
|
|
99
|
+
const indexes = await extractIndexes(
|
|
100
|
+
mockPoolSequence(
|
|
101
|
+
// attempt 1: definition is NULL (transient race)
|
|
102
|
+
[{ ...baseRow, name: '"racy_idx"', definition: null }],
|
|
103
|
+
// attempt 2: catalog scan no longer sees the dropped row, or
|
|
104
|
+
// pg_get_indexdef successfully resolves the definition
|
|
105
|
+
[
|
|
106
|
+
{
|
|
107
|
+
...baseRow,
|
|
108
|
+
name: '"racy_idx"',
|
|
109
|
+
definition: "CREATE INDEX racy_idx ON users (id)",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
{ retries: 2, backoffMs: 0 },
|
|
114
|
+
);
|
|
115
|
+
expect(indexes).toHaveLength(1);
|
|
116
|
+
expect(indexes[0]?.name).toBe('"racy_idx"');
|
|
117
|
+
expect(indexes[0]?.definition).toBe("CREATE INDEX racy_idx ON users (id)");
|
|
118
|
+
});
|
|
83
119
|
});
|
|
@@ -2,6 +2,10 @@ import { sql } from "@ts-safeql/sql-tag";
|
|
|
2
2
|
import type { Pool } from "pg";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { BasePgModel } from "../base.model.ts";
|
|
5
|
+
import {
|
|
6
|
+
type ExtractRetryOptions,
|
|
7
|
+
extractWithDefinitionRetry,
|
|
8
|
+
} from "../extract-with-retry.ts";
|
|
5
9
|
|
|
6
10
|
const TableRelkindSchema = z.enum([
|
|
7
11
|
"r", // table (regular relation)
|
|
@@ -163,7 +167,11 @@ export class Index extends BasePgModel {
|
|
|
163
167
|
nulls_not_distinct: this.nulls_not_distinct,
|
|
164
168
|
immediate: this.immediate,
|
|
165
169
|
is_clustered: this.is_clustered,
|
|
166
|
-
is_replica_identity:
|
|
170
|
+
// is_replica_identity excluded: the table's `replica_identity` /
|
|
171
|
+
// `replica_identity_index` is the source of truth, set via
|
|
172
|
+
// ALTER TABLE ... REPLICA IDENTITY USING INDEX. Including this flag here
|
|
173
|
+
// would trigger spurious DROP+CREATE of the index whenever the table's
|
|
174
|
+
// replica identity changes.
|
|
167
175
|
// key_columns excluded: contains attribute numbers that can differ between databases
|
|
168
176
|
// even when indexes are logically identical. The definition field already captures
|
|
169
177
|
// the logical structure using column names, so we compare by definition instead.
|
|
@@ -215,8 +223,16 @@ export class Index extends BasePgModel {
|
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
225
|
|
|
218
|
-
export async function extractIndexes(
|
|
219
|
-
|
|
226
|
+
export async function extractIndexes(
|
|
227
|
+
pool: Pool,
|
|
228
|
+
options?: ExtractRetryOptions,
|
|
229
|
+
): Promise<Index[]> {
|
|
230
|
+
const indexRows = await extractWithDefinitionRetry({
|
|
231
|
+
label: "indexes",
|
|
232
|
+
options,
|
|
233
|
+
hasNullDefinition: (row) => row.definition === null,
|
|
234
|
+
query: async () => {
|
|
235
|
+
const result = await pool.query<IndexProps>(sql`
|
|
220
236
|
with extension_oids as (
|
|
221
237
|
select objid
|
|
222
238
|
from pg_depend d
|
|
@@ -372,8 +388,11 @@ export async function extractIndexes(pool: Pool): Promise<Index[]> {
|
|
|
372
388
|
|
|
373
389
|
order by 1, 2
|
|
374
390
|
`);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
return result.rows.map((row: unknown) => indexRowSchema.parse(row));
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
const validatedRows = indexRows.filter(
|
|
395
|
+
(row): row is IndexProps => row.definition !== null,
|
|
396
|
+
);
|
|
378
397
|
return validatedRows.map((row: IndexProps) => new Index(row));
|
|
379
398
|
}
|