db: Drizzle schemas on domain Postgres schemas, baseline migration, reorg SQL

- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta
  (lists excluded via schemaFilter; runtime list tables are managed by meta.*)
- drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal)
- db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy)
- scripts/db-migrate.ts: migrator with --init to record the baseline and a guard
  against running the baseline on a non-empty, unrecorded database
This commit is contained in:
2026-10-23 22:03:42 +00:00
parent f94e3dc8ac
commit 64fb8df262
29 changed files with 12507 additions and 189 deletions

View File

@@ -1,47 +1,92 @@
import { boolean, index, integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { id, timestamps } from './helpers';
import { user } from './auth';
import {
boolean,
check,
index,
integer,
jsonb,
text,
timestamp,
unique
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { metaSchema } from './schemas';
export const lists = pgTable('lists', {
id: id(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
pictureUrl: text('picture_url'),
isProgress: boolean('is_progress').notNull().default(false),
...timestamps
});
// Registry of GUI-visible tables. Runtime lists live in schema `lists` (one real
// table per list, created through meta.create_list()); that schema is excluded
// from drizzle-kit via schemaFilter. Structure truth is pg_catalog; these rows are
// presentation. See src/lib/server/lists.ts for the list engine.
const ts = (name: string) => timestamp(name, { withTimezone: true });
export const listItems = pgTable(
'list_items',
export const metaTables = metaSchema.table(
'tables',
{
id: id(),
listId: integer('list_id')
.notNull()
.references(() => lists.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
pictureUrl: text('picture_url'),
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
schemaName: text('schema_name').notNull(),
tableName: text('table_name').notNull(),
slug: text('slug').notNull().unique('tables_slug_key'),
displayName: text('display_name').notNull(),
description: text('description'),
...timestamps
icon: text('icon'),
pictureUrl: text('picture_url'),
kind: text('kind', { enum: ['list', 'core', 'ref'] }).notNull(),
editable: boolean('editable').notNull().default(true),
hasDone: boolean('has_done').notNull().default(false),
defaultSort: text('default_sort'),
sortOrder: integer('sort_order').notNull().default(0),
archivedAt: ts('archived_at'),
createdAt: ts('created_at').notNull().defaultNow(),
updatedAt: ts('updated_at').notNull().defaultNow()
},
(table) => [index().on(table.listId)]
(t) => [
unique('tables_schema_name_table_name_key').on(t.schemaName, t.tableName),
check('tables_slug_check', sql`slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'`),
check('tables_kind_check', sql`kind IN ('list','core','ref')`)
]
);
export const listItemCompletions = pgTable(
'list_item_completions',
export const uiTypes = [
'text',
'longtext',
'url',
'image',
'number',
'cents',
'date',
'datetime',
'bool',
'select',
'multiselect',
'fk',
'json'
] as const;
export type UiType = (typeof uiTypes)[number];
export const metaColumns = metaSchema.table(
'columns',
{
id: id(),
itemId: integer('item_id')
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
tableId: integer('table_id')
.notNull()
.references(() => listItems.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
...timestamps
.references(() => metaTables.id, { onDelete: 'cascade' }),
columnName: text('column_name').notNull(),
label: text('label'),
uiType: text('ui_type', { enum: uiTypes }).notNull(),
options: jsonb('options').$type<{ values?: string[] }>().notNull().default({}),
position: integer('position').notNull().default(0),
hidden: boolean('hidden').notNull().default(false),
helpText: text('help_text'),
createdAt: ts('created_at').notNull().defaultNow(),
updatedAt: ts('updated_at').notNull().defaultNow()
},
(table) => [unique().on(table.itemId, table.userId), index().on(table.userId)]
(t) => [
unique('columns_table_id_column_name_key').on(t.tableId, t.columnName),
index('columns_table_id_index').on(t.tableId),
check(
'columns_ui_type_check',
sql`ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json')`
)
]
);
export type List = typeof lists.$inferSelect;
export type NewList = typeof lists.$inferInsert;
export type ListItem = typeof listItems.$inferSelect;
export type NewListItem = typeof listItems.$inferInsert;
export type MetaTable = typeof metaTables.$inferSelect;
export type MetaColumn = typeof metaColumns.$inferSelect;