kryptheon-night 0.1.1 → 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 +141 -5
package/package.json
CHANGED
package/schema.js
CHANGED
|
@@ -499,6 +499,57 @@ function rewriteSchemaRefs(expr, fromSchema, toSchema) {
|
|
|
499
499
|
return String(expr).split(quote(fromSchema) + '.').join(quote(toSchema) + '.').split(fromSchema + '.').join(toSchema + '.');
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Points a reference with no schema on it at the copy.
|
|
504
|
+
*
|
|
505
|
+
* The one the other two rewrites could not see. `pg_get_constraintdef` writes
|
|
506
|
+
* the schema only when the referenced table is *not* reachable through
|
|
507
|
+
* `search_path` - so an app living in `app_something` comes back as
|
|
508
|
+
* "REFERENCES app_something.profiles(id)" and is rewritten, while the same app
|
|
509
|
+
* living in `public` comes back as "REFERENCES profiles(id)" and there is
|
|
510
|
+
* nothing to rewrite. Replayed into the copy, that bare name resolves through
|
|
511
|
+
* `search_path` all over again, and binds to the customer's real table.
|
|
512
|
+
*
|
|
513
|
+
* Found on the first real Supabase project this was ever pointed at, which is
|
|
514
|
+
* the first app that was not in a schema of its own:
|
|
515
|
+
*
|
|
516
|
+
* notes_owner_fkey -> public.profiles (the customer's own table)
|
|
517
|
+
* orders_user_id_fkey -> public.profiles (the customer's own table)
|
|
518
|
+
*
|
|
519
|
+
* Nothing was written to those tables - the copy only pointed at them - but
|
|
520
|
+
* the copy was not the app, which is the same failure three other bugs in this
|
|
521
|
+
* file were. Here it meant every insert into the copy was checked against a
|
|
522
|
+
* table the seeder had put nothing in, so two tables out of four were never
|
|
523
|
+
* attacked and were reported, honestly but uselessly, as not checked.
|
|
524
|
+
*
|
|
525
|
+
* Only names the plan itself owns are touched. A reference to something
|
|
526
|
+
* outside the schema is the stand-in's business, not this one's.
|
|
527
|
+
*/
|
|
528
|
+
function qualifyOwnRefs(definition, plan, target) {
|
|
529
|
+
const own = new Set((plan.tables || []).map((table) => table.name));
|
|
530
|
+
// A replacer function, never a replacement string: `$&` and friends are read
|
|
531
|
+
// as instructions inside one, and that has already cost this project an
|
|
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.
|
|
542
|
+
return String(definition).replace(
|
|
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
|
+
},
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
502
553
|
/**
|
|
503
554
|
* Points a foreign key at the stand-in instead of at the real outside table.
|
|
504
555
|
*
|
|
@@ -669,9 +720,13 @@ async function writeSchema(client, plan, target) {
|
|
|
669
720
|
for (const constraint of table.constraints) {
|
|
670
721
|
const isForeign = constraint.kind === 'f';
|
|
671
722
|
if (isForeign !== wantForeign) continue;
|
|
672
|
-
const definition =
|
|
673
|
-
|
|
674
|
-
|
|
723
|
+
const definition = qualifyOwnRefs(
|
|
724
|
+
rewriteExternalRefs(
|
|
725
|
+
rewriteSchemaRefs(constraint.definition, plan.schema, target),
|
|
726
|
+
plan.external,
|
|
727
|
+
target,
|
|
728
|
+
),
|
|
729
|
+
plan,
|
|
675
730
|
target,
|
|
676
731
|
);
|
|
677
732
|
statements.push(
|
|
@@ -767,9 +822,89 @@ async function writeSchema(client, plan, target) {
|
|
|
767
822
|
}
|
|
768
823
|
|
|
769
824
|
mustStayInside(statements, target);
|
|
770
|
-
|
|
771
|
-
|
|
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(() => {});
|
|
772
856
|
}
|
|
857
|
+
|
|
858
|
+
// And then look at what was actually built, rather than at what was meant.
|
|
859
|
+
//
|
|
860
|
+
// Everything above this line reasons about strings. The guard at the top of
|
|
861
|
+
// this file reads the statements before they run; this one asks Postgres
|
|
862
|
+
// where the copy ended up pointing, which is the only account of it that
|
|
863
|
+
// cannot be fooled by a spelling nobody anticipated - and one was not. A
|
|
864
|
+
// bare "REFERENCES profiles(id)" replayed into the copy bound to the
|
|
865
|
+
// customer's own table, silently, on every app that lives in `public`.
|
|
866
|
+
//
|
|
867
|
+
// Nothing is written to a table outside the copy either way. The damage is
|
|
868
|
+
// subtler than that: a copy tied to the customer's rows is not the app being
|
|
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.
|
|
880
|
+
const { rows: strays } = await client.query(
|
|
881
|
+
`SELECT con.conname AS what, rn.nspname AS points_at
|
|
882
|
+
FROM pg_constraint con
|
|
883
|
+
JOIN pg_class cl ON cl.oid = con.conrelid
|
|
884
|
+
JOIN pg_namespace cn ON cn.oid = cl.relnamespace
|
|
885
|
+
JOIN pg_class rc ON rc.oid = con.confrelid
|
|
886
|
+
JOIN pg_namespace rn ON rn.oid = rc.relnamespace
|
|
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'`,
|
|
899
|
+
[target],
|
|
900
|
+
);
|
|
901
|
+
if (strays.length) {
|
|
902
|
+
throw new Error(
|
|
903
|
+
'the copy points outside itself, so it is not the app: ' +
|
|
904
|
+
strays.map((row) => row.what + ' -> ' + row.points_at).join(', '),
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
773
908
|
return statements;
|
|
774
909
|
}
|
|
775
910
|
|
|
@@ -915,6 +1050,7 @@ module.exports = {
|
|
|
915
1050
|
readExternalTargets: readExternalTargets,
|
|
916
1051
|
referenceIn: referenceIn,
|
|
917
1052
|
rewriteSchemaRefs: rewriteSchemaRefs,
|
|
1053
|
+
qualifyOwnRefs: qualifyOwnRefs,
|
|
918
1054
|
stubNameFor: stubNameFor,
|
|
919
1055
|
IDENTITIES: IDENTITIES,
|
|
920
1056
|
quote: quote,
|