a plugin install creates its own tables and cannot touch anything else
The logs from the last commit showed what was actually wrong, and it was worse
than a failed push.
── What the install was doing ──
`bun db:push` hands drizzle-kit the whole schema and the whole database and asks
it to make them match. On a machine holding tables the current schema does not
declare — a plugin that is not on disk, anything left from before an extraction —
it sees a new table appearing while others are due to be dropped, and asks:
Is offscale_servers table created or renamed from another table?
❯ + offscale_servers create table
~ headscale_servers › offscale_servers rename table
~ music_favorites › offscale_servers rename table
That prompt needs a TTY. `--force` does not answer it, `< /dev/null` does not
answer it, and piping newlines does not either — measured: it hangs until killed.
Without a terminal the push applies NOTHING and exits 0, which is exactly how an
install reported success on a machine where its table was never created.
And `--force` would have been the wrong fix. Force means "run the destructive
statements without asking", and the destructive statements there are `drop table`
for every plugin whose directory is absent. Installing one plugin could delete
another's data.
── `tablesFilter` was tried, and is not enough ──
Scoping the push to the plugin's tables stops it dropping other tables and it
still diffs sequences. Measured on a scratch database: installing offscale issued
`drop sequence passkey_challenges_id_seq` — a CORE platform sequence — because the
plugin's schema does not declare it. Postgres refused only because a column
default depended on it. A sequence without dependents would have gone.
── So: generate the SQL, then apply it ──
`drizzle-kit generate` against the plugin's schema alone, into an empty directory,
emits exactly the statements that build that schema from nothing. It diffs against
the migration journal — empty — not against the database, so it cannot emit a DROP
of anything, of any object type. There is no destructive statement to guard.
Applying is idempotent by exception: an object that exists raises 42P07/42710 and
is skipped, which is the normal case for a reinstall, since uninstall keeps data
on purpose.
`[open]` This creates; it does not migrate. A plugin version that adds a column
will not get it. Plugin upgrades need real migrations — a separate feature. The
old push appeared to handle that case, at the price of being able to drop half the
database.
── Verified ──
Fresh install: 4 statements applied, table verified, 761ms. Reinstall over
surviving tables: 0 applied, 4 already present. Core sequences before and after:
18. Orphan tables on a scratch database shaped like the failing server
(headscale_servers, music_favorites): untouched.
The reinstall case failed on the first attempt — drizzle wraps driver errors, so
the SQLSTATE saying "duplicate table" is on `cause`, and checking only the top
level made every second install fail on its own CREATE TABLE. Found by testing the
second install rather than stopping at the first.
tsgo clean, frontend builds, 873 pass / 7 fail unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { discoverPlugins } from './discover';
|
||||
import { fetchPlugin } from './fetch';
|
||||
import { publishAssets, unpublishAssets } from '../app-store/assets';
|
||||
import { refreshPluginMounts, snapshotPlugins } from './mount';
|
||||
import { generatePluginSchemas, pushSchema } from './schema';
|
||||
import { generatePluginSchemas, pushPluginSchema } from './schema';
|
||||
import { declaredTables, dropTables, existingTables } from './tables';
|
||||
import type { OnLog } from './run-logged';
|
||||
import { installDependencies, manualInstallHint, reportDependencies, stillMissing } from './os-deps';
|
||||
@@ -228,21 +228,24 @@ export async function installPlugin(
|
||||
const { plugins } = await discoverPlugins();
|
||||
generatePluginSchemas(plugins);
|
||||
|
||||
const pushed = await pushSchema(onLog);
|
||||
// The table list first: it is both what the push is scoped to and what is verified afterwards.
|
||||
const { tables, refused } = await declaredTables(plugin);
|
||||
|
||||
const pushed = await pushPluginSchema(plugin, tables, onLog);
|
||||
if (!pushed.ok) {
|
||||
return { ok: false, appName, steps, error: `schema push failed: ${pushed.output}` };
|
||||
}
|
||||
|
||||
// Exit code 0 is not proof the tables exist.
|
||||
//
|
||||
// `db:push` is a compound command — generate the barrel, cd, run drizzle-kit — and an install was
|
||||
// reported as successful on a machine where `offscale_servers` was never created. The plugin then
|
||||
// An install reported success on a machine where `offscale_servers` was never created: the plugin
|
||||
// mounted, its sidecar started, registered, and failed its first query with 42P01. Every step said
|
||||
// OK and the plugin was unusable, which is the worst shape a failure can take.
|
||||
// OK and the plugin was unusable, which is the worst shape a failure can take. The scoped push
|
||||
// above removes the cause; this stays because "the command exited 0" and "the tables are there"
|
||||
// are different claims, and only one of them is the one that matters.
|
||||
//
|
||||
// So the install asks Postgres. A missing table fails the install HERE, where the message can say
|
||||
// what happened, instead of appearing later as a stack trace in a sidecar log.
|
||||
const { tables, refused } = await declaredTables(plugin);
|
||||
if (refused.length) {
|
||||
// Not fatal: the plugin still works, and the tables are still created by push. But they are
|
||||
// outside its prefix, so `purge` will not remove them and nobody will notice until a reinstall
|
||||
|
||||
Reference in New Issue
Block a user