supaschema 0.1.0-rc.1

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.
Files changed (257) hide show
  1. package/.agents/skills/supaschema/SKILL.md +61 -0
  2. package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
  3. package/.claude/rules/supaschema.md +22 -0
  4. package/.claude/settings.json +16 -0
  5. package/.claude/skills/supaschema/SKILL.md +61 -0
  6. package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
  7. package/.codex/hooks.json +16 -0
  8. package/.codex/rules/supaschema.rules +22 -0
  9. package/AGENTS.md +40 -0
  10. package/LICENSE +661 -0
  11. package/LICENSE-COMMERCIAL.md +35 -0
  12. package/README.md +249 -0
  13. package/benchmarks/README.md +104 -0
  14. package/benchmarks/compare.js +489 -0
  15. package/benchmarks/fixtures/additive/from.sql +8 -0
  16. package/benchmarks/fixtures/additive/manifest.json +1 -0
  17. package/benchmarks/fixtures/additive/to.sql +9 -0
  18. package/benchmarks/fixtures/functions-policies/from.sql +24 -0
  19. package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
  20. package/benchmarks/fixtures/functions-policies/to.sql +24 -0
  21. package/benchmarks/plot-lib.js +234 -0
  22. package/benchmarks/plot-svg.js +339 -0
  23. package/benchmarks/plot.js +154 -0
  24. package/benchmarks/tools/bench-all.sh +49 -0
  25. package/benchmarks/tools/build-project-fixture.mjs +245 -0
  26. package/benchmarks/tools/compare-db.mjs +101 -0
  27. package/benchmarks/tools/compare-fixtures.mjs +84 -0
  28. package/benchmarks/tools/compare-report.mjs +90 -0
  29. package/benchmarks/tools/compare-supabase.mjs +67 -0
  30. package/benchmarks/tools/registry.js +266 -0
  31. package/benchmarks/tools/run-workflow.mjs +77 -0
  32. package/bin/postinstall.mjs +26 -0
  33. package/bin/supaschema +2 -0
  34. package/config-schema.json +208 -0
  35. package/corpus/supabase-style/corpus.json +6 -0
  36. package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
  37. package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
  38. package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
  39. package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
  40. package/corpus/supabase-style/roles.sql +13 -0
  41. package/corpus/supabase-style/tree/functions.sql +26 -0
  42. package/corpus/supabase-style/tree/policies.sql +4 -0
  43. package/corpus/supabase-style/tree/schema.sql +4 -0
  44. package/corpus/supabase-style/tree/tables.sql +20 -0
  45. package/corpus/supabase-style/tree/triggers.sql +3 -0
  46. package/corpus/supabase-style/tree/types.sql +2 -0
  47. package/corpus/supabase-style/tree/views.sql +6 -0
  48. package/dist/audit.d.ts +20 -0
  49. package/dist/audit.d.ts.map +1 -0
  50. package/dist/audit.js +68 -0
  51. package/dist/benchmark-db.d.ts +5 -0
  52. package/dist/benchmark-db.d.ts.map +1 -0
  53. package/dist/benchmark-db.js +71 -0
  54. package/dist/benchmark-fixtures.d.ts +10 -0
  55. package/dist/benchmark-fixtures.d.ts.map +1 -0
  56. package/dist/benchmark-fixtures.js +201 -0
  57. package/dist/benchmark.d.ts +2 -0
  58. package/dist/benchmark.d.ts.map +1 -0
  59. package/dist/benchmark.js +308 -0
  60. package/dist/catalog-comments.d.ts +9 -0
  61. package/dist/catalog-comments.d.ts.map +1 -0
  62. package/dist/catalog-comments.js +194 -0
  63. package/dist/catalog-extras.d.ts +12 -0
  64. package/dist/catalog-extras.d.ts.map +1 -0
  65. package/dist/catalog-extras.js +408 -0
  66. package/dist/catalog-foreign.d.ts +15 -0
  67. package/dist/catalog-foreign.d.ts.map +1 -0
  68. package/dist/catalog-foreign.js +114 -0
  69. package/dist/catalog-tables.d.ts +9 -0
  70. package/dist/catalog-tables.d.ts.map +1 -0
  71. package/dist/catalog-tables.js +114 -0
  72. package/dist/catalog.d.ts +8 -0
  73. package/dist/catalog.d.ts.map +1 -0
  74. package/dist/catalog.js +351 -0
  75. package/dist/check-hazards.d.ts +7 -0
  76. package/dist/check-hazards.d.ts.map +1 -0
  77. package/dist/check-hazards.js +83 -0
  78. package/dist/check-reporters.d.ts +8 -0
  79. package/dist/check-reporters.d.ts.map +1 -0
  80. package/dist/check-reporters.js +76 -0
  81. package/dist/check.d.ts +3 -0
  82. package/dist/check.d.ts.map +1 -0
  83. package/dist/check.js +229 -0
  84. package/dist/cli-defaults.d.ts +24 -0
  85. package/dist/cli-defaults.d.ts.map +1 -0
  86. package/dist/cli-defaults.js +65 -0
  87. package/dist/cli-diff.d.ts +13 -0
  88. package/dist/cli-diff.d.ts.map +1 -0
  89. package/dist/cli-diff.js +348 -0
  90. package/dist/cli-reports.d.ts +9 -0
  91. package/dist/cli-reports.d.ts.map +1 -0
  92. package/dist/cli-reports.js +90 -0
  93. package/dist/cli-tools.d.ts +17 -0
  94. package/dist/cli-tools.d.ts.map +1 -0
  95. package/dist/cli-tools.js +136 -0
  96. package/dist/cli.d.ts +2 -0
  97. package/dist/cli.d.ts.map +1 -0
  98. package/dist/cli.js +239 -0
  99. package/dist/config-schema-gen.d.ts +2 -0
  100. package/dist/config-schema-gen.d.ts.map +1 -0
  101. package/dist/config-schema-gen.js +11 -0
  102. package/dist/config.d.ts +58 -0
  103. package/dist/config.d.ts.map +1 -0
  104. package/dist/config.js +132 -0
  105. package/dist/core.d.ts +115 -0
  106. package/dist/core.d.ts.map +1 -0
  107. package/dist/core.js +1 -0
  108. package/dist/corpus.d.ts +26 -0
  109. package/dist/corpus.d.ts.map +1 -0
  110. package/dist/corpus.js +112 -0
  111. package/dist/database-url.d.ts +8 -0
  112. package/dist/database-url.d.ts.map +1 -0
  113. package/dist/database-url.js +74 -0
  114. package/dist/db-admin.d.ts +23 -0
  115. package/dist/db-admin.d.ts.map +1 -0
  116. package/dist/db-admin.js +147 -0
  117. package/dist/diagnostics.d.ts +16 -0
  118. package/dist/diagnostics.d.ts.map +1 -0
  119. package/dist/diagnostics.js +155 -0
  120. package/dist/diff-score.d.ts +12 -0
  121. package/dist/diff-score.d.ts.map +1 -0
  122. package/dist/diff-score.js +339 -0
  123. package/dist/doctor.d.ts +17 -0
  124. package/dist/doctor.d.ts.map +1 -0
  125. package/dist/doctor.js +110 -0
  126. package/dist/hash.d.ts +7 -0
  127. package/dist/hash.d.ts.map +1 -0
  128. package/dist/hash.js +34 -0
  129. package/dist/index.d.ts +24 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +17 -0
  132. package/dist/lineage.d.ts +23 -0
  133. package/dist/lineage.d.ts.map +1 -0
  134. package/dist/lineage.js +61 -0
  135. package/dist/migrations-status.d.ts +35 -0
  136. package/dist/migrations-status.d.ts.map +1 -0
  137. package/dist/migrations-status.js +131 -0
  138. package/dist/plan-order.d.ts +4 -0
  139. package/dist/plan-order.d.ts.map +1 -0
  140. package/dist/plan-order.js +178 -0
  141. package/dist/planner-replace.d.ts +4 -0
  142. package/dist/planner-replace.d.ts.map +1 -0
  143. package/dist/planner-replace.js +76 -0
  144. package/dist/planner-table.d.ts +3 -0
  145. package/dist/planner-table.d.ts.map +1 -0
  146. package/dist/planner-table.js +165 -0
  147. package/dist/planner.d.ts +5 -0
  148. package/dist/planner.d.ts.map +1 -0
  149. package/dist/planner.js +385 -0
  150. package/dist/render-guards.d.ts +12 -0
  151. package/dist/render-guards.d.ts.map +1 -0
  152. package/dist/render-guards.js +159 -0
  153. package/dist/render.d.ts +7 -0
  154. package/dist/render.d.ts.map +1 -0
  155. package/dist/render.js +325 -0
  156. package/dist/selfcheck.d.ts +11 -0
  157. package/dist/selfcheck.d.ts.map +1 -0
  158. package/dist/selfcheck.js +43 -0
  159. package/dist/source-normalize.d.ts +14 -0
  160. package/dist/source-normalize.d.ts.map +1 -0
  161. package/dist/source-normalize.js +420 -0
  162. package/dist/source.d.ts +4 -0
  163. package/dist/source.d.ts.map +1 -0
  164. package/dist/source.js +233 -0
  165. package/dist/sql/ast.d.ts +42 -0
  166. package/dist/sql/ast.d.ts.map +1 -0
  167. package/dist/sql/ast.js +241 -0
  168. package/dist/sql/canonical-nodes.d.ts +5 -0
  169. package/dist/sql/canonical-nodes.d.ts.map +1 -0
  170. package/dist/sql/canonical-nodes.js +101 -0
  171. package/dist/sql/extract-helpers.d.ts +18 -0
  172. package/dist/sql/extract-helpers.d.ts.map +1 -0
  173. package/dist/sql/extract-helpers.js +127 -0
  174. package/dist/sql/extract.d.ts +13 -0
  175. package/dist/sql/extract.d.ts.map +1 -0
  176. package/dist/sql/extract.js +323 -0
  177. package/dist/sql/facts.d.ts +34 -0
  178. package/dist/sql/facts.d.ts.map +1 -0
  179. package/dist/sql/facts.js +392 -0
  180. package/dist/sql/identifiers.d.ts +13 -0
  181. package/dist/sql/identifiers.d.ts.map +1 -0
  182. package/dist/sql/identifiers.js +83 -0
  183. package/dist/sql/normalize-deparse.d.ts +25 -0
  184. package/dist/sql/normalize-deparse.d.ts.map +1 -0
  185. package/dist/sql/normalize-deparse.js +96 -0
  186. package/dist/sql/object-hash.d.ts +5 -0
  187. package/dist/sql/object-hash.d.ts.map +1 -0
  188. package/dist/sql/object-hash.js +24 -0
  189. package/dist/sql/parser.d.ts +8 -0
  190. package/dist/sql/parser.d.ts.map +1 -0
  191. package/dist/sql/parser.js +89 -0
  192. package/dist/sql/privileges.d.ts +33 -0
  193. package/dist/sql/privileges.d.ts.map +1 -0
  194. package/dist/sql/privileges.js +379 -0
  195. package/dist/sql/split.d.ts +3 -0
  196. package/dist/sql/split.d.ts.map +1 -0
  197. package/dist/sql/split.js +182 -0
  198. package/dist/sql/statements.d.ts +17 -0
  199. package/dist/sql/statements.d.ts.map +1 -0
  200. package/dist/sql/statements.js +284 -0
  201. package/dist/sql/table-constraints.d.ts +15 -0
  202. package/dist/sql/table-constraints.d.ts.map +1 -0
  203. package/dist/sql/table-constraints.js +304 -0
  204. package/dist/sql/table-shape.d.ts +38 -0
  205. package/dist/sql/table-shape.d.ts.map +1 -0
  206. package/dist/sql/table-shape.js +287 -0
  207. package/dist/sync.d.ts +27 -0
  208. package/dist/sync.d.ts.map +1 -0
  209. package/dist/sync.js +86 -0
  210. package/dist/typegen-model.d.ts +78 -0
  211. package/dist/typegen-model.d.ts.map +1 -0
  212. package/dist/typegen-model.js +338 -0
  213. package/dist/typegen-views.d.ts +7 -0
  214. package/dist/typegen-views.d.ts.map +1 -0
  215. package/dist/typegen-views.js +92 -0
  216. package/dist/typegen-zod.d.ts +3 -0
  217. package/dist/typegen-zod.d.ts.map +1 -0
  218. package/dist/typegen-zod.js +149 -0
  219. package/dist/typegen.d.ts +4 -0
  220. package/dist/typegen.d.ts.map +1 -0
  221. package/dist/typegen.js +184 -0
  222. package/dist/validators.d.ts +3 -0
  223. package/dist/validators.d.ts.map +1 -0
  224. package/dist/validators.js +104 -0
  225. package/dist/verify-environment.d.ts +5 -0
  226. package/dist/verify-environment.d.ts.map +1 -0
  227. package/dist/verify-environment.js +92 -0
  228. package/dist/verify.d.ts +3 -0
  229. package/dist/verify.d.ts.map +1 -0
  230. package/dist/verify.js +261 -0
  231. package/docs/benchmarks/additive-correctness.svg +86 -0
  232. package/docs/benchmarks/additive-latency.svg +60 -0
  233. package/docs/benchmarks/functions-policies-correctness.svg +86 -0
  234. package/docs/benchmarks/functions-policies-latency.svg +60 -0
  235. package/docs/benchmarks/realistic-correctness.svg +86 -0
  236. package/docs/benchmarks/realistic-latency.svg +60 -0
  237. package/docs/benchmarks/scaling-latency.svg +106 -0
  238. package/docs/benchmarks/workflow-latency.svg +98 -0
  239. package/docs/benchmarks/xl-correctness.svg +86 -0
  240. package/docs/benchmarks/xl-latency.svg +60 -0
  241. package/docs/benchmarks/xxl-correctness.svg +86 -0
  242. package/docs/benchmarks/xxl-latency.svg +66 -0
  243. package/docs/case-study-anilize.md +51 -0
  244. package/docs/ci-gate.md +44 -0
  245. package/docs/ci.md +68 -0
  246. package/docs/commands.md +35 -0
  247. package/docs/config.md +72 -0
  248. package/docs/corpus.md +33 -0
  249. package/docs/diagnostics.md +77 -0
  250. package/docs/hints.md +92 -0
  251. package/docs/release.md +19 -0
  252. package/docs/support-matrix.md +57 -0
  253. package/examples/postgres/schemas/001_app.sql +17 -0
  254. package/examples/supabase/schemas/001_app.sql +13 -0
  255. package/examples/supabase/schemas-next/001_app.sql +21 -0
  256. package/examples/supabase/supaschema.config.json +15 -0
  257. package/package.json +99 -0
@@ -0,0 +1,208 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "supaschema configuration",
4
+ "type": "object",
5
+ "properties": {
6
+ "$schema": {
7
+ "type": "string"
8
+ },
9
+ "adapter": {
10
+ "default": "supabase-auto",
11
+ "type": "string",
12
+ "enum": [
13
+ "supabase-auto",
14
+ "postgres"
15
+ ]
16
+ },
17
+ "cascade": {
18
+ "default": "never",
19
+ "type": "string",
20
+ "const": "never"
21
+ },
22
+ "destructiveChanges": {
23
+ "default": "hint-required",
24
+ "type": "string",
25
+ "enum": [
26
+ "hint-required",
27
+ "block",
28
+ "allow"
29
+ ]
30
+ },
31
+ "environments": {
32
+ "default": {},
33
+ "type": "object",
34
+ "propertyNames": {
35
+ "type": "string"
36
+ },
37
+ "additionalProperties": {
38
+ "type": "object",
39
+ "properties": {
40
+ "databaseUrl": {
41
+ "type": "string"
42
+ }
43
+ },
44
+ "required": [
45
+ "databaseUrl"
46
+ ],
47
+ "additionalProperties": false
48
+ }
49
+ },
50
+ "excludedGrantRoles": {
51
+ "default": [],
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ }
56
+ },
57
+ "hints": {
58
+ "default": {
59
+ "destructive": [],
60
+ "renames": []
61
+ },
62
+ "type": "object",
63
+ "properties": {
64
+ "destructive": {
65
+ "default": [],
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ }
70
+ },
71
+ "renames": {
72
+ "default": [],
73
+ "type": "array",
74
+ "items": {
75
+ "type": "object",
76
+ "properties": {
77
+ "from": {
78
+ "type": "string"
79
+ },
80
+ "to": {
81
+ "type": "string"
82
+ }
83
+ },
84
+ "required": [
85
+ "from",
86
+ "to"
87
+ ],
88
+ "additionalProperties": false
89
+ }
90
+ }
91
+ }
92
+ },
93
+ "idempotency": {
94
+ "default": "required",
95
+ "type": "string",
96
+ "const": "required"
97
+ },
98
+ "lockTimeout": {
99
+ "default": "5s",
100
+ "type": "string"
101
+ },
102
+ "migrationsDir": {
103
+ "default": "supabase/migrations",
104
+ "type": "string"
105
+ },
106
+ "typesFile": {
107
+ "default": "database.types.ts",
108
+ "type": "string"
109
+ },
110
+ "zodFile": {
111
+ "default": "database.zod.ts",
112
+ "type": "string"
113
+ },
114
+ "normalize": {
115
+ "default": "deparse",
116
+ "type": "string",
117
+ "enum": [
118
+ "off",
119
+ "deparse"
120
+ ]
121
+ },
122
+ "managedSchemas": {
123
+ "default": [
124
+ "auth",
125
+ "storage",
126
+ "realtime",
127
+ "vault",
128
+ "extensions",
129
+ "cron",
130
+ "net",
131
+ "supabase_functions",
132
+ "graphql",
133
+ "graphql_public"
134
+ ],
135
+ "type": "array",
136
+ "items": {
137
+ "type": "string"
138
+ }
139
+ },
140
+ "postgresVersion": {
141
+ "default": "15+",
142
+ "type": "string"
143
+ },
144
+ "renameDetection": {
145
+ "default": "hints-only",
146
+ "type": "string",
147
+ "enum": [
148
+ "hints-only",
149
+ "off"
150
+ ]
151
+ },
152
+ "schemaPaths": {
153
+ "default": [
154
+ "supabase/schemas"
155
+ ],
156
+ "type": "array",
157
+ "items": {
158
+ "type": "string"
159
+ }
160
+ },
161
+ "schemas": {
162
+ "default": {
163
+ "exclude": [],
164
+ "include": []
165
+ },
166
+ "type": "object",
167
+ "properties": {
168
+ "exclude": {
169
+ "default": [],
170
+ "type": "array",
171
+ "items": {
172
+ "type": "string"
173
+ }
174
+ },
175
+ "include": {
176
+ "default": [],
177
+ "type": "array",
178
+ "items": {
179
+ "type": "string"
180
+ }
181
+ }
182
+ },
183
+ "additionalProperties": false
184
+ },
185
+ "statementTimeout": {
186
+ "default": "60s",
187
+ "type": "string"
188
+ },
189
+ "transactionMode": {
190
+ "default": "per-migration",
191
+ "type": "string",
192
+ "enum": [
193
+ "per-migration",
194
+ "per-statement"
195
+ ]
196
+ },
197
+ "validators": {
198
+ "default": [
199
+ "internal-parser"
200
+ ],
201
+ "type": "array",
202
+ "items": {
203
+ "type": "string"
204
+ }
205
+ }
206
+ },
207
+ "additionalProperties": false
208
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "adapter": "postgres",
3
+ "schemas": { "exclude": [], "include": ["app"] },
4
+ "excludedGrantRoles": [],
5
+ "hints": { "destructive": [], "renames": [] }
6
+ }
@@ -0,0 +1,28 @@
1
+ CREATE SCHEMA app;
2
+ GRANT USAGE ON SCHEMA app TO authenticated, anon;
3
+
4
+ CREATE FUNCTION app.current_tenant() RETURNS uuid
5
+ LANGUAGE sql STABLE
6
+ SET search_path = ''
7
+ AS $$ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid $$;
8
+
9
+ CREATE TABLE app.accounts (
10
+ id bigint NOT NULL,
11
+ tenant_id uuid NOT NULL,
12
+ balance numeric NOT NULL,
13
+ status text NOT NULL
14
+ );
15
+
16
+ CREATE SEQUENCE app.accounts_id_seq;
17
+ ALTER SEQUENCE app.accounts_id_seq OWNED BY app.accounts.id;
18
+ ALTER TABLE app.accounts ALTER COLUMN id SET DEFAULT nextval('"app"."accounts_id_seq"');
19
+
20
+ ALTER TABLE app.accounts ENABLE ROW LEVEL SECURITY;
21
+ ALTER TABLE ONLY app.accounts FORCE ROW LEVEL SECURITY;
22
+
23
+ CREATE POLICY accounts_select ON app.accounts
24
+ FOR SELECT
25
+ TO authenticated
26
+ USING (tenant_id = (SELECT app.current_tenant()) AND balance >= 0);
27
+
28
+ GRANT SELECT ON app.accounts TO authenticated;
@@ -0,0 +1,13 @@
1
+ GRANT EXECUTE ON FUNCTION app.current_tenant() TO authenticated;
2
+
3
+ REVOKE ALL ON SCHEMA app FROM PUBLIC;
4
+
5
+ ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE SELECT ON TABLES FROM anon;
6
+
7
+ ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO service_role;
8
+
9
+ CREATE TYPE app.account_status AS ENUM ('active', 'closed');
10
+ GRANT USAGE ON TYPE app.account_status TO authenticated;
11
+
12
+ COMMENT ON TABLE app.accounts IS 'Customer accounts';
13
+ COMMENT ON FUNCTION app.current_tenant() IS 'Tenant resolver';
@@ -0,0 +1,4 @@
1
+ GRANT INSERT ON app.accounts TO anon;
2
+ REVOKE INSERT ON app.accounts FROM anon;
3
+
4
+ REVOKE EXECUTE ON FUNCTION app.current_tenant() FROM PUBLIC;
@@ -0,0 +1,17 @@
1
+ CREATE FUNCTION app.touch_account_audit()
2
+ RETURNS trigger
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ SET search_path TO ''
6
+ AS $function$
7
+ BEGIN
8
+ IF NEW.status IS DISTINCT FROM OLD.status THEN
9
+ NEW.status := lower(NEW.status);
10
+ END IF;
11
+ RETURN NEW;
12
+ END;
13
+ $function$;
14
+ COMMENT ON FUNCTION app.touch_account_audit() IS 'Normalizes status on update';
15
+ REVOKE ALL ON FUNCTION app.touch_account_audit() FROM PUBLIC;
16
+ GRANT EXECUTE ON FUNCTION app.touch_account_audit() TO service_role;
17
+ CREATE OR REPLACE TRIGGER accounts_touch_audit BEFORE UPDATE ON app.accounts FOR EACH ROW EXECUTE FUNCTION app.touch_account_audit();
@@ -0,0 +1,13 @@
1
+ DO $supaschema$
2
+ BEGIN
3
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'anon') THEN
4
+ CREATE ROLE anon NOLOGIN NOINHERIT;
5
+ END IF;
6
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'authenticated') THEN
7
+ CREATE ROLE authenticated NOLOGIN NOINHERIT;
8
+ END IF;
9
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'service_role') THEN
10
+ CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS;
11
+ END IF;
12
+ END
13
+ $supaschema$;
@@ -0,0 +1,26 @@
1
+ create function app.current_tenant() returns uuid
2
+ language sql stable
3
+ set search_path = ''
4
+ as $$ select nullif(current_setting('app.tenant_id', true), '')::uuid $$;
5
+
6
+ revoke execute on function app.current_tenant() from public;
7
+ grant execute on function app.current_tenant() to authenticated;
8
+
9
+ comment on function app.current_tenant() is 'Tenant resolver';
10
+
11
+ create function app.touch_account_audit() returns trigger
12
+ language plpgsql security definer
13
+ set search_path = ''
14
+ as $$
15
+ begin
16
+ if NEW.status is distinct from OLD.status then
17
+ NEW.status := lower(NEW.status);
18
+ end if;
19
+ return NEW;
20
+ end;
21
+ $$;
22
+
23
+ revoke all on function app.touch_account_audit() from public;
24
+ grant execute on function app.touch_account_audit() to service_role;
25
+
26
+ comment on function app.touch_account_audit() is 'Normalizes status on update';
@@ -0,0 +1,4 @@
1
+ create policy accounts_select on app.accounts
2
+ for select
3
+ to authenticated
4
+ using (tenant_id = (select app.current_tenant()) and balance >= 0);
@@ -0,0 +1,4 @@
1
+ create schema app;
2
+ grant usage on schema app to authenticated, anon;
3
+
4
+ alter default privileges in schema app grant select on tables to service_role;
@@ -0,0 +1,20 @@
1
+ create sequence app.accounts_id_seq;
2
+
3
+ create table app.accounts (
4
+ id BIGINT not null default nextval('app.accounts_id_seq'),
5
+ tenant_id uuid not null,
6
+ balance numeric not null,
7
+ status text not null,
8
+ note text
9
+ );
10
+
11
+ alter sequence app.accounts_id_seq owned by app.accounts.id;
12
+
13
+ alter table app.accounts enable row level security;
14
+ alter table only app.accounts force row level security;
15
+
16
+ create index accounts_tenant_id_idx on app.accounts (tenant_id);
17
+
18
+ grant select on app.accounts to authenticated;
19
+
20
+ comment on table app.accounts is 'Customer accounts';
@@ -0,0 +1,3 @@
1
+ create or replace trigger accounts_touch_audit
2
+ before update on app.accounts
3
+ for each row execute function app.touch_account_audit();
@@ -0,0 +1,2 @@
1
+ create type app.account_status as enum ('active', 'closed');
2
+ grant usage on type app.account_status to authenticated;
@@ -0,0 +1,6 @@
1
+ create view app.account_summary as
2
+ select tenant_id, count(*) as account_count, sum(balance) as total_balance
3
+ from app.accounts
4
+ group by tenant_id;
5
+
6
+ grant select on app.account_summary to authenticated;
@@ -0,0 +1,20 @@
1
+ import type { SchemaModel } from "./core.js";
2
+ export interface AuditFinding {
3
+ code: string;
4
+ count: number;
5
+ message: string;
6
+ samples: string[];
7
+ severity: string;
8
+ }
9
+ export interface AuditReport {
10
+ errorStatements: number;
11
+ findings: AuditFinding[];
12
+ objectsByKind: Record<string, number>;
13
+ objectsBySchema: Record<string, number>;
14
+ source: string;
15
+ supported: boolean;
16
+ totalObjects: number;
17
+ }
18
+ export declare function auditModel(model: SchemaModel): AuditReport;
19
+ export declare function renderAuditReport(report: AuditReport): string;
20
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,WAAW,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAKD,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CA0C1D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CA0B7D"}
package/dist/audit.js ADDED
@@ -0,0 +1,68 @@
1
+ const sampleLimit = 5;
2
+ const sampleLength = 120;
3
+ export function auditModel(model) {
4
+ const objectsByKind = {};
5
+ const objectsBySchema = {};
6
+ for (const object of model.objects) {
7
+ objectsByKind[object.ref.kind] = (objectsByKind[object.ref.kind] ?? 0) + 1;
8
+ const schema = object.ref.schema ?? (object.ref.kind === "schema" ? object.ref.name : "-");
9
+ objectsBySchema[schema] = (objectsBySchema[schema] ?? 0) + 1;
10
+ }
11
+ const groups = new Map();
12
+ for (const item of model.diagnostics) {
13
+ if (item.severity !== "error" && item.severity !== "warning") {
14
+ continue;
15
+ }
16
+ const group = groups.get(item.code) ?? { diagnostics: [], severity: item.severity };
17
+ group.diagnostics.push(item);
18
+ groups.set(item.code, group);
19
+ }
20
+ const findings = [...groups.entries()]
21
+ .map(([code, group]) => ({
22
+ code,
23
+ count: group.diagnostics.length,
24
+ message: group.diagnostics[0]?.message ?? "",
25
+ samples: group.diagnostics
26
+ .map((item) => item.statement ?? item.hint ?? "")
27
+ .filter(Boolean)
28
+ .slice(0, sampleLimit)
29
+ .map((sample) => sample.replaceAll(/\s+/gu, " ").slice(0, sampleLength)),
30
+ severity: group.severity,
31
+ }))
32
+ .sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
33
+ const errorStatements = findings
34
+ .filter((finding) => finding.severity === "error")
35
+ .reduce((total, finding) => total + finding.count, 0);
36
+ return {
37
+ errorStatements,
38
+ findings,
39
+ objectsByKind,
40
+ objectsBySchema,
41
+ source: model.source,
42
+ supported: errorStatements === 0,
43
+ totalObjects: model.objects.length,
44
+ };
45
+ }
46
+ export function renderAuditReport(report) {
47
+ const lines = [];
48
+ lines.push(`supaschema audit: ${report.source}`);
49
+ lines.push(`objects modeled: ${report.totalObjects} across ${Object.keys(report.objectsBySchema).length} schema(s)`);
50
+ const kinds = Object.entries(report.objectsByKind)
51
+ .sort((left, right) => right[1] - left[1])
52
+ .map(([kind, count]) => `${kind}=${count}`)
53
+ .join(" ");
54
+ lines.push(`by kind: ${kinds || "none"}`);
55
+ if (report.findings.length === 0) {
56
+ lines.push("contract coverage: full — no findings");
57
+ return `${lines.join("\n")}\n`;
58
+ }
59
+ lines.push(`contract coverage: ${report.errorStatements} statement(s) outside the contract, ${report.findings.length} finding class(es)`);
60
+ for (const finding of report.findings) {
61
+ lines.push(` ${finding.severity.toUpperCase()} ${finding.code} ×${finding.count}`);
62
+ for (const sample of finding.samples) {
63
+ lines.push(` ${sample}`);
64
+ }
65
+ }
66
+ lines.push("run `supaschema explain <CODE>` for remediation guidance per code");
67
+ return `${lines.join("\n")}\n`;
68
+ }
@@ -0,0 +1,5 @@
1
+ export declare function withTemporaryDatabases<T>(databaseUrl: string, count: number, callback: (databaseUrls: string[]) => Promise<T>): Promise<T>;
2
+ export declare function applySql(databaseUrl: string, sql: string): Promise<void>;
3
+ export declare function applyMigrationSql(databaseUrl: string, sql: string): Promise<void>;
4
+ export declare function databasePair(databaseUrls: string[]): [string, string];
5
+ //# sourceMappingURL=benchmark-db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark-db.d.ts","sourceRoot":"","sources":["../src/benchmark-db.ts"],"names":[],"mappings":"AAIA,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAC/C,OAAO,CAAC,CAAC,CAAC,CA2BZ;AAED,wBAAsB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9E;AAID,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvF;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrE"}
@@ -0,0 +1,71 @@
1
+ import { Client } from "pg";
2
+ import { quoteIdent } from "./sql/identifiers.js";
3
+ import { splitSqlStatements } from "./sql/split.js";
4
+ export async function withTemporaryDatabases(databaseUrl, count, callback) {
5
+ const admin = new Client({ connectionString: databaseUrl });
6
+ const created = [];
7
+ try {
8
+ await admin.connect();
9
+ for (let index = 0; index < count; index += 1) {
10
+ const databaseName = tempDatabaseName(index);
11
+ await admin.query(`CREATE DATABASE ${quoteIdent(databaseName)}`);
12
+ created.push(databaseName);
13
+ }
14
+ return await callback(created.map((databaseName) => databaseUrlWithDatabase(databaseUrl, databaseName)));
15
+ }
16
+ finally {
17
+ for (const databaseName of created.reverse()) {
18
+ await admin
19
+ .query(`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [databaseName])
20
+ .catch(() => undefined);
21
+ await admin
22
+ .query(`DROP DATABASE IF EXISTS ${quoteIdent(databaseName)} WITH (FORCE)`)
23
+ .catch(() => undefined);
24
+ }
25
+ await admin.end().catch(() => undefined);
26
+ }
27
+ }
28
+ export async function applySql(databaseUrl, sql) {
29
+ const client = new Client({ connectionString: databaseUrl });
30
+ try {
31
+ await client.connect();
32
+ for (const statement of splitSqlStatements(sql)) {
33
+ await client.query(statement);
34
+ }
35
+ }
36
+ finally {
37
+ await client.end().catch(() => undefined);
38
+ }
39
+ }
40
+ // One transaction per migration mirrors runners like `supabase db push`; an
41
+ // error leaves the transaction uncommitted and ending the connection aborts it.
42
+ export async function applyMigrationSql(databaseUrl, sql) {
43
+ const client = new Client({ connectionString: databaseUrl });
44
+ try {
45
+ await client.connect();
46
+ await client.query("BEGIN");
47
+ for (const statement of splitSqlStatements(sql)) {
48
+ await client.query(statement);
49
+ }
50
+ await client.query("COMMIT");
51
+ }
52
+ finally {
53
+ await client.end().catch(() => undefined);
54
+ }
55
+ }
56
+ export function databasePair(databaseUrls) {
57
+ const [fromUrl, toUrl] = databaseUrls;
58
+ if (!fromUrl || !toUrl) {
59
+ throw new Error("expected two temporary benchmark databases");
60
+ }
61
+ return [fromUrl, toUrl];
62
+ }
63
+ function databaseUrlWithDatabase(databaseUrl, databaseName) {
64
+ const url = new URL(databaseUrl);
65
+ url.pathname = `/${databaseName}`;
66
+ return url.toString();
67
+ }
68
+ function tempDatabaseName(index) {
69
+ const suffix = `${process.pid}_${Date.now()}_${index}_${Math.random().toString(16).slice(2)}`;
70
+ return `pg_diverge_benchmark_${suffix}`.replace(/[^A-Za-z0-9_]/g, "_").slice(0, 60);
71
+ }
@@ -0,0 +1,10 @@
1
+ export interface FixtureManifestEntry {
2
+ change: "create" | "change";
3
+ key: string;
4
+ }
5
+ export declare function realisticFixtureManifest(tableCount: number): FixtureManifestEntry[];
6
+ export declare function makeRealisticSqlFixture(tableCount: number): {
7
+ from: string;
8
+ to: string;
9
+ };
10
+ //# sourceMappingURL=benchmark-fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark-fixtures.d.ts","sourceRoot":"","sources":["../src/benchmark-fixtures.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAwFnF;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAgDxF"}