kryptheon-night 0.1.2 → 0.1.3
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/package.json +1 -1
- package/schema.js +72 -8
package/package.json
CHANGED
package/schema.js
CHANGED
|
@@ -530,10 +530,23 @@ function qualifyOwnRefs(definition, plan, target) {
|
|
|
530
530
|
// A replacer function, never a replacement string: `$&` and friends are read
|
|
531
531
|
// as instructions inside one, and that has already cost this project an
|
|
532
532
|
// engine that would not install.
|
|
533
|
+
// A quoted identifier can hold anything, including spaces and quotes of its
|
|
534
|
+
// own doubled up, so it is matched as a whole rather than as a run of safe
|
|
535
|
+
// characters. The first attempt used [^".\s(]+ inside the quotes and
|
|
536
|
+
// silently did not match `REFERENCES "Group Table"(...)` - which the twin
|
|
537
|
+
// fixture has, because it was built out of every shape that has ever been
|
|
538
|
+
// read wrong, and it caught this the first time it ran.
|
|
539
|
+
//
|
|
540
|
+
// The unquoted alternative excludes a dot, so a name that is already
|
|
541
|
+
// schema-qualified is left alone: that is the other rewrite's work.
|
|
533
542
|
return String(definition).replace(
|
|
534
|
-
/(\bREFERENCES\s+)("
|
|
535
|
-
(whole, before,
|
|
536
|
-
|
|
543
|
+
/(\bREFERENCES\s+)("(?:[^"]|"")*"|[^\s(".]+)(\s*\()/gi,
|
|
544
|
+
(whole, before, written, after) => {
|
|
545
|
+
const name = written.charAt(0) === '"'
|
|
546
|
+
? written.slice(1, -1).split('""').join('"')
|
|
547
|
+
: written;
|
|
548
|
+
return own.has(name) ? before + quote(target) + '.' + quote(name) + after : whole;
|
|
549
|
+
},
|
|
537
550
|
);
|
|
538
551
|
}
|
|
539
552
|
|
|
@@ -809,8 +822,37 @@ async function writeSchema(client, plan, target) {
|
|
|
809
822
|
}
|
|
810
823
|
|
|
811
824
|
mustStayInside(statements, target);
|
|
812
|
-
|
|
813
|
-
|
|
825
|
+
// Built with the copy first on the search_path, so a bare name binds to the
|
|
826
|
+
// copy and not to whatever the customer happens to have.
|
|
827
|
+
//
|
|
828
|
+
// This is the structural half, and it went in after chasing the same bug
|
|
829
|
+
// through three different kinds of expression. Postgres writes a name
|
|
830
|
+
// without its schema whenever that name is already reachable - so a real app
|
|
831
|
+
// in `public` hands back "REFERENCES profiles(id)" and
|
|
832
|
+
// "nextval('orders_id_seq'::regclass)", and a rewrite that goes looking for
|
|
833
|
+
// a schema name finds nothing to change in either. Replayed into the copy,
|
|
834
|
+
// both bound to the customer's own objects: the foreign keys pointed at
|
|
835
|
+
// their tables, and the copy drew its keys from their sequences, which
|
|
836
|
+
// advanced them.
|
|
837
|
+
//
|
|
838
|
+
// Rewriting each kind of expression in turn is a game with no end - index
|
|
839
|
+
// predicates, checks that call a function, view bodies. Naming the copy
|
|
840
|
+
// first on the path ends all of them at once, because it changes what a
|
|
841
|
+
// bare name means rather than trying to find every place one can appear.
|
|
842
|
+
//
|
|
843
|
+
// `public` stays on the path, after the copy, because the copy legitimately
|
|
844
|
+
// needs what lives there - gen_random_uuid() and the like. `extensions` is
|
|
845
|
+
// where Supabase keeps them; a schema on the path that does not exist is
|
|
846
|
+
// ignored rather than an error.
|
|
847
|
+
const { rows: pathRows } = await client.query('SHOW search_path');
|
|
848
|
+
const restoreTo = pathRows[0].search_path;
|
|
849
|
+
await client.query('SET search_path TO ' + quote(target) + ', public, extensions');
|
|
850
|
+
try {
|
|
851
|
+
for (const statement of statements) {
|
|
852
|
+
await client.query(statement);
|
|
853
|
+
}
|
|
854
|
+
} finally {
|
|
855
|
+
await client.query('SET search_path TO ' + restoreTo).catch(() => {});
|
|
814
856
|
}
|
|
815
857
|
|
|
816
858
|
// And then look at what was actually built, rather than at what was meant.
|
|
@@ -825,20 +867,41 @@ async function writeSchema(client, plan, target) {
|
|
|
825
867
|
// Nothing is written to a table outside the copy either way. The damage is
|
|
826
868
|
// subtler than that: a copy tied to the customer's rows is not the app being
|
|
827
869
|
// attacked, and every verdict taken from it is about something else.
|
|
870
|
+
// Foreign keys AND column defaults, because the second one is where this
|
|
871
|
+
// hid after the first was closed: a serial column's default is
|
|
872
|
+
// nextval('<sequence>'), and the copy was calling the customer's. Nothing
|
|
873
|
+
// was written to their tables, but nextval advances a sequence, so their
|
|
874
|
+
// database changed - and "we never touch your live app" says at all.
|
|
875
|
+
//
|
|
876
|
+
// Asked of pg_depend rather than of the text, so it holds for any
|
|
877
|
+
// expression that ends up pointing at a relation, not only the ones anybody
|
|
878
|
+
// thought to look at. pg_catalog is excluded because everything depends on
|
|
879
|
+
// it; anything else outside the copy is the bug.
|
|
828
880
|
const { rows: strays } = await client.query(
|
|
829
|
-
`SELECT con.conname, rn.nspname AS points_at
|
|
881
|
+
`SELECT con.conname AS what, rn.nspname AS points_at
|
|
830
882
|
FROM pg_constraint con
|
|
831
883
|
JOIN pg_class cl ON cl.oid = con.conrelid
|
|
832
884
|
JOIN pg_namespace cn ON cn.oid = cl.relnamespace
|
|
833
885
|
JOIN pg_class rc ON rc.oid = con.confrelid
|
|
834
886
|
JOIN pg_namespace rn ON rn.oid = rc.relnamespace
|
|
835
|
-
WHERE con.contype = 'f' AND cn.nspname = $1 AND rn.nspname <> $1
|
|
887
|
+
WHERE con.contype = 'f' AND cn.nspname = $1 AND rn.nspname <> $1
|
|
888
|
+
UNION ALL
|
|
889
|
+
SELECT cl.relname || '.' || a.attname || ' default' AS what,
|
|
890
|
+
rn.nspname || '.' || rc.relname AS points_at
|
|
891
|
+
FROM pg_depend d
|
|
892
|
+
JOIN pg_attrdef ad ON ad.oid = d.objid AND d.classid = 'pg_attrdef'::regclass
|
|
893
|
+
JOIN pg_class cl ON cl.oid = ad.adrelid
|
|
894
|
+
JOIN pg_namespace cn ON cn.oid = cl.relnamespace
|
|
895
|
+
JOIN pg_attribute a ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
|
|
896
|
+
JOIN pg_class rc ON rc.oid = d.refobjid AND d.refclassid = 'pg_class'::regclass
|
|
897
|
+
JOIN pg_namespace rn ON rn.oid = rc.relnamespace
|
|
898
|
+
WHERE cn.nspname = $1 AND rn.nspname <> $1 AND rn.nspname <> 'pg_catalog'`,
|
|
836
899
|
[target],
|
|
837
900
|
);
|
|
838
901
|
if (strays.length) {
|
|
839
902
|
throw new Error(
|
|
840
903
|
'the copy points outside itself, so it is not the app: ' +
|
|
841
|
-
strays.map((row) => row.
|
|
904
|
+
strays.map((row) => row.what + ' -> ' + row.points_at).join(', '),
|
|
842
905
|
);
|
|
843
906
|
}
|
|
844
907
|
|
|
@@ -987,6 +1050,7 @@ module.exports = {
|
|
|
987
1050
|
readExternalTargets: readExternalTargets,
|
|
988
1051
|
referenceIn: referenceIn,
|
|
989
1052
|
rewriteSchemaRefs: rewriteSchemaRefs,
|
|
1053
|
+
qualifyOwnRefs: qualifyOwnRefs,
|
|
990
1054
|
stubNameFor: stubNameFor,
|
|
991
1055
|
IDENTITIES: IDENTITIES,
|
|
992
1056
|
quote: quote,
|