name the over-long foreign keys so db:push can be a no-op
Drizzle generates names past Postgres's 63-character identifier limit. Postgres stores the truncated form, drizzle compares against its own longer one, and every push on every host drops and re-adds constraints that are already identical. Deferred as harmless noise since 2026-08-19. What ended that was a deploy plan saying "expect no changes, stop if it wants to alter something" — which this made impossible to satisfy, so the deploy agent had to overrule its own instructions. Verified by pushing twice against a scratch database: the fleet-wide second-push churn went from ten statements to two, and the two that remain are a composite primary key that cannot be fixed either way. Naming a foreign key means removing the inline .references() or drizzle creates both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+40
-22
@@ -1,4 +1,4 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex, foreignKey} from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from 'officerdb/auth/schema';
|
import { users } from 'officerdb/auth/schema';
|
||||||
|
|
||||||
@@ -72,8 +72,7 @@ export const walletLabels = pgTable(
|
|||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
walletId: integer('wallet_id')
|
walletId: integer('wallet_id')
|
||||||
.notNull()
|
.notNull(),
|
||||||
.references(() => walletWallets.id, { onDelete: 'cascade' }),
|
|
||||||
// 'address' | 'tx'
|
// 'address' | 'tx'
|
||||||
kind: text('kind').notNull(),
|
kind: text('kind').notNull(),
|
||||||
// The address string or txid being labelled.
|
// The address string or txid being labelled.
|
||||||
@@ -96,14 +95,22 @@ export const walletFrozenUtxos = pgTable(
|
|||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
walletId: integer('wallet_id')
|
walletId: integer('wallet_id')
|
||||||
.notNull()
|
.notNull(),
|
||||||
.references(() => walletWallets.id, { onDelete: 'cascade' }),
|
|
||||||
// `txid:vout`
|
// `txid:vout`
|
||||||
outpoint: text('outpoint').notNull(),
|
outpoint: text('outpoint').notNull(),
|
||||||
reason: text('reason'),
|
reason: text('reason'),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(t) => [uniqueIndex('uq_bitcoin_wallet_frozen_utxos_wallet_outpoint').on(t.walletId, t.outpoint)],
|
(t) => [
|
||||||
|
// Named, because drizzle's generated name exceeds Postgres's 63-character identifier limit.
|
||||||
|
// Postgres stores the truncated form, drizzle keeps comparing against its own longer one, and
|
||||||
|
// every `db:push` drops and re-adds an identical constraint — on every host, for ever.
|
||||||
|
foreignKey({
|
||||||
|
name: 'fk_bitcoin_wallet_frozen_utxos_wallet',
|
||||||
|
columns: [t.walletId],
|
||||||
|
foreignColumns: [walletWallets.id],
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
uniqueIndex('uq_bitcoin_wallet_frozen_utxos_wallet_outpoint').on(t.walletId, t.outpoint)],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,19 +164,30 @@ export type WalletChainSnapshot = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const walletChainCache = pgTable('bitcoin_wallet_chain_cache', {
|
export const walletChainCache = pgTable(
|
||||||
// One row per wallet, so a refresh is a single atomic upsert with no partially-written window.
|
'bitcoin_wallet_chain_cache',
|
||||||
walletId: integer('wallet_id')
|
{
|
||||||
.primaryKey()
|
// One row per wallet, so a refresh is a single atomic upsert with no partially-written window.
|
||||||
.references(() => walletWallets.id, { onDelete: 'cascade' }),
|
walletId: integer('wallet_id').primaryKey(),
|
||||||
// Nullable: a wallet whose very first refresh failed has an error to report and no data to report it
|
// Nullable: a wallet whose very first refresh failed has an error to report and no data to report it
|
||||||
// against, and that row still has to exist for the UI to say why the screen is empty.
|
// against, and that row still has to exist for the UI to say why the screen is empty.
|
||||||
snapshot: jsonb('snapshot').$type<WalletChainSnapshot>(),
|
snapshot: jsonb('snapshot').$type<WalletChainSnapshot>(),
|
||||||
/** When the chain was last read successfully — the age the UI shows. Not when the row was written. */
|
/** When the chain was last read successfully — the age the UI shows. Not when the row was written. */
|
||||||
syncedAt: timestamp('synced_at', { withTimezone: true }),
|
syncedAt: timestamp('synced_at', { withTimezone: true }),
|
||||||
// Kept ALONGSIDE the last good snapshot rather than replacing it. A failure that wiped the data would
|
// Kept ALONGSIDE the last good snapshot rather than replacing it. A failure that wiped the data would
|
||||||
// reproduce the exact bug this table exists to fix: an unreachable upstream rendering as a zero balance
|
// reproduce the exact bug this table exists to fix: an unreachable upstream rendering as a zero balance
|
||||||
// instead of as a stale one.
|
// instead of as a stale one.
|
||||||
lastError: text('last_error'),
|
lastError: text('last_error'),
|
||||||
lastErrorAt: timestamp('last_error_at', { withTimezone: true }),
|
lastErrorAt: timestamp('last_error_at', { withTimezone: true }),
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
// Named, because drizzle's generated name is 65 characters and Postgres truncates at 63 — it stores
|
||||||
|
// the short form, drizzle compares against its own long one, and every push drops and re-adds an
|
||||||
|
// identical constraint. Same fix as `fk_bitcoin_wallet_frozen_utxos_wallet` above.
|
||||||
|
foreignKey({
|
||||||
|
name: 'fk_bitcoin_wallet_chain_cache_wallet',
|
||||||
|
columns: [t.walletId],
|
||||||
|
foreignColumns: [walletWallets.id],
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user