Files
pastilhasandClaude Opus 5 e8cc90b34c 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>
2026-08-23 14:17:04 +00:00

61 lines
2.9 KiB
TypeScript

import { pgTable, uuid, text, timestamp, index, primaryKey, foreignKey} from 'drizzle-orm/pg-core';
import { generations } from '../generations/schema';
/**
* A named set of images — an afternoon's work, an icon suite, a client.
*
* `name` is unique, and that is load-bearing rather than tidy: the generation API accepts a
* collection by name and creates it when it does not exist, so without the constraint a second
* request with the same name would quietly start a second collection and split the batch in two.
*/
export const collections = pgTable('atelier_collections', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull().unique(),
description: text('description'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
/**
* Membership, as a join table rather than a column on `generations`.
*
* An image belongs to as many collections as it earns: the icon that came out of Tuesday's session is
* in "tuesday" and also in "shipped", and removing it from one must not touch the other. A
* collection_id column would force a choice at generation time and make re-filing a destructive edit.
*
* Both sides cascade. Deleting a collection removes its memberships and NOT its images — a collection
* is a way of looking at generations, not a container that owns them. Deleting a generation removes
* its memberships, since a row pointing at nothing is only a way to crash a join later.
*/
export const collectionItems = pgTable('atelier_collection_items',
{
collectionId: uuid('collection_id').notNull(),
generationId: uuid('generation_id').notNull(),
addedAt: timestamp('added_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
/*
Both foreign keys are NAMED, because drizzle generates names over Postgres's 63-character identifier
limit — 65 and 64. Postgres stores the truncated form, drizzle keeps comparing against its own longer
one, and every `db:push` drops and re-adds constraints that are already identical. Four statements per
push, on every host, for ever.
It read as harmless noise until a deploy plan said "expect no changes" and this made that impossible
to satisfy on all five hosts, forcing the deploy agent to overrule its own instructions.
*/
foreignKey({
name: 'fk_atelier_collection_items_collection',
columns: [table.collectionId],
foreignColumns: [collections.id],
}).onDelete('cascade'),
foreignKey({
name: 'fk_atelier_collection_items_generation',
columns: [table.generationId],
foreignColumns: [generations.id],
}).onDelete('cascade'),
// The composite key is what makes adding an image twice a no-op instead of a duplicate.
primaryKey({ columns: [table.collectionId, table.generationId] }),
index('atelier_collection_items_generation_idx').on(table.generationId),
],
);