diff --git a/AGENTS.md b/AGENTS.md
index ec53041..ee6694c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -47,7 +47,7 @@ Use impeccable for user-facing UI: `craft` for new UI, `audit`/`polish` on exist
- Plugins (`plugins/*`) import only `@sttracker/plugin-sdk`. They never import `packages/db` or core
internals, and they return `SourceRecord`s instead of writing.
- `packages/*` and `plugins/*` never import from `apps/*`.
-- IDs are ULIDs generated in app code. Ratings are integers 0–100. Dates carry a precision
+- IDs are UUIDv7 (`newId()`) generated in app code. Ratings are integers 0–100. Dates carry a precision
(`day | month | year | unknown`).
- Tests never call live APIs. Plugin tests use recorded fixtures through the SDK test harness.
- Write-back (`push`) is opt-in per connection and off by default. No plugin pushes in Phase 1.
diff --git a/packages/db/src/migrations/0001_initial.ts b/packages/db/src/migrations/0001_initial.ts
new file mode 100644
index 0000000..ad3fdec
--- /dev/null
+++ b/packages/db/src/migrations/0001_initial.ts
@@ -0,0 +1,392 @@
+import type { Kysely } from "kysely";
+import type { Migration } from "kysely/migration";
+
+/**
+ * Column kinds: `t` text, `i` integer, `r` real, `fk:
` text reference. Not null by default;
+ * a trailing `?` makes it nullable. Only portable types, so the same code runs on SQLite and Postgres.
+ */
+type Kind = string;
+type Spec = Array<[name: string, kind: Kind]>;
+
+async function create(
+ db: Kysely,
+ name: string,
+ cols: Spec,
+ opts: { unique?: string[][]; indexes?: string[][] } = {},
+): Promise {
+ let b = db.schema.createTable(name).addColumn("id", "text", (c) => c.primaryKey());
+ const fkCols: string[] = [];
+ for (const [col, kind] of cols) {
+ const nullable = kind.endsWith("?");
+ const base = nullable ? kind.slice(0, -1) : kind;
+ const isFk = base.startsWith("fk:");
+ const type = base === "i" ? "integer" : base === "r" ? "real" : "text";
+ if (isFk) fkCols.push(col);
+ b = b.addColumn(col, type, (c) => {
+ let x = c;
+ if (isFk) x = x.references(`${base.slice(3)}.id`);
+ return nullable ? x : x.notNull();
+ });
+ }
+ for (const u of opts.unique ?? []) b = b.addUniqueConstraint(`${name}_${u.join("_")}_uq`, u as Array<"id">);
+ await b.execute();
+
+ const indexes = [...fkCols.map((c) => [c]), ...(opts.indexes ?? [])];
+ for (const cols of indexes) {
+ await db.schema
+ .createIndex(`${name}_${cols.join("_")}_idx`)
+ .on(name)
+ .columns(cols as Array<"id">)
+ .execute();
+ }
+}
+
+export const initial: Migration = {
+ async up(db: Kysely) {
+ await create(
+ db,
+ "users",
+ [
+ ["username", "t"],
+ ["display_name", "t"],
+ ["password_hash", "t"],
+ ["role", "t"],
+ ["preferences", "t"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ],
+ { unique: [["username"]] },
+ );
+ await create(
+ db,
+ "sessions",
+ [
+ ["user_id", "fk:users"],
+ ["token_hash", "t"],
+ ["expires_at", "t"],
+ ["created_at", "t"],
+ ],
+ { unique: [["token_hash"]] },
+ );
+ await create(
+ db,
+ "invites",
+ [
+ ["code_hash", "t"],
+ ["created_by", "fk:users"],
+ ["used_by", "fk:users?"],
+ ["expires_at", "t"],
+ ["created_at", "t"],
+ ],
+ { unique: [["code_hash"]] },
+ );
+ await create(
+ db,
+ "app_tokens",
+ [
+ ["user_id", "fk:users"],
+ ["name", "t"],
+ ["token_hash", "t"],
+ ["scopes", "t"],
+ ["last_used_at", "t?"],
+ ["revoked_at", "t?"],
+ ["created_at", "t"],
+ ],
+ { unique: [["token_hash"]] },
+ );
+ // works.parent_id is a self reference; works.cover_attachment_id and merged_into_id stay
+ // plain columns (attachments is created later; merges may point at any work).
+ await create(
+ db,
+ "works",
+ [
+ ["media_type", "t"],
+ ["title", "t"],
+ ["original_title", "t"],
+ ["year", "i?"],
+ ["release_date", "t?"],
+ ["creators", "t"],
+ ["description", "t"],
+ ["cover_url", "t"],
+ ["cover_attachment_id", "t?"],
+ ["metadata", "t"],
+ ["parent_id", "fk:works?"],
+ ["position", "t?"],
+ ["owner_user_id", "fk:users?"],
+ ["merged_into_id", "t?"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ],
+ { indexes: [["merged_into_id"], ["media_type"]] },
+ );
+ await create(
+ db,
+ "external_ids",
+ [
+ ["work_id", "fk:works"],
+ ["namespace", "t"],
+ ["value", "t"],
+ ],
+ { unique: [["namespace", "value"]] },
+ );
+ await create(
+ db,
+ "work_aliases",
+ [
+ ["user_id", "fk:users"],
+ ["from_work_id", "fk:works"],
+ ["to_work_id", "fk:works"],
+ ],
+ { unique: [["user_id", "from_work_id"]] },
+ );
+ await create(db, "work_history", [
+ ["work_id", "fk:works"],
+ ["before", "t"],
+ ["after", "t"],
+ ["actor", "t"],
+ ["created_at", "t"],
+ ]);
+ await create(
+ db,
+ "entries",
+ [
+ ["user_id", "fk:users"],
+ ["work_id", "fk:works"],
+ ["status", "t"],
+ ["rating", "i?"],
+ ["review", "t?"],
+ ["private_notes", "t?"],
+ ["favorite", "i"],
+ ["tags", "t"],
+ ["custom_fields", "t"],
+ ["visibility", "t"],
+ ["overrides", "t"],
+ ["media_type_override", "t?"],
+ ["field_sources", "t"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ["deleted_at", "t?"],
+ ],
+ { unique: [["user_id", "work_id"]] },
+ );
+ await create(db, "engagements", [
+ ["entry_id", "fk:entries"],
+ ["started_at", "t?"],
+ ["started_precision", "t"],
+ ["finished_at", "t?"],
+ ["finished_precision", "t"],
+ ["progress", "t"],
+ ["platform", "t?"],
+ ["rating", "i?"],
+ ["notes", "t?"],
+ ["custom_fields", "t"],
+ ["field_sources", "t"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ["deleted_at", "t?"],
+ ]);
+ await create(
+ db,
+ "source_connections",
+ [
+ ["user_id", "fk:users"],
+ ["plugin_id", "t"],
+ ["name", "t"],
+ ["config", "t"],
+ ["credentials_enc", "t?"],
+ ["schedule_minutes", "i?"],
+ ["cursor", "t?"],
+ ["status", "t"],
+ ["last_run_at", "t?"],
+ ["created_at", "t"],
+ ],
+ );
+ await create(
+ db,
+ "engagement_sources",
+ [
+ ["engagement_id", "fk:engagements"],
+ ["connection_id", "fk:source_connections"],
+ ["record_key", "t"],
+ ],
+ { unique: [["connection_id", "record_key"]] },
+ );
+ await create(
+ db,
+ "listens",
+ [
+ ["user_id", "fk:users"],
+ ["listened_at", "t"],
+ ["artist", "t"],
+ ["track", "t"],
+ ["album", "t?"],
+ ["duration_ms", "i?"],
+ ["mbids", "t"],
+ ["work_id", "fk:works?"],
+ ["source", "t"],
+ ["source_key", "t"],
+ ["created_at", "t"],
+ ],
+ { unique: [["user_id", "source", "source_key"]], indexes: [["user_id", "listened_at"]] },
+ );
+ await create(db, "lists", [
+ ["user_id", "fk:users"],
+ ["name", "t"],
+ ["description", "t"],
+ ["kind", "t"],
+ ["visibility", "t"],
+ ["rating", "i?"],
+ ["review", "t?"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ["deleted_at", "t?"],
+ ]);
+ await create(db, "list_items", [
+ ["list_id", "fk:lists"],
+ ["work_id", "fk:works"],
+ ["position", "i"],
+ ["note", "t?"],
+ ]);
+ await create(db, "custom_field_definitions", [
+ ["user_id", "fk:users"],
+ ["name", "t"],
+ ["type", "t"],
+ ["options", "t"],
+ ["applies_to", "t"],
+ ["level", "t"],
+ ["created_at", "t"],
+ ]);
+ await create(db, "copies", [
+ ["user_id", "fk:users"],
+ ["work_id", "fk:works?"],
+ ["title_hint", "t?"],
+ ["format", "t?"],
+ ["acquired_from", "t?"],
+ ["acquired_at", "t?"],
+ ["acquired_precision", "t"],
+ ["price_cents", "i?"],
+ ["currency", "t?"],
+ ["notes", "t?"],
+ ["custom_fields", "t"],
+ ["source_ref", "t?"],
+ ["created_at", "t"],
+ ["updated_at", "t"],
+ ["deleted_at", "t?"],
+ ]);
+ await create(
+ db,
+ "attachments",
+ [
+ ["user_id", "fk:users"],
+ ["sha256", "t"],
+ ["mime", "t"],
+ ["size", "i"],
+ ["filename", "t"],
+ ["caption", "t?"],
+ ["taken_at", "t?"],
+ ["visibility", "t?"],
+ ["created_at", "t"],
+ ],
+ { indexes: [["sha256"]] },
+ );
+ await create(
+ db,
+ "attachment_links",
+ [
+ ["attachment_id", "fk:attachments"],
+ ["entity", "t"],
+ ["entity_id", "t"],
+ ],
+ { indexes: [["entity", "entity_id"]] },
+ );
+ await create(
+ db,
+ "user_history",
+ [
+ ["user_id", "fk:users"],
+ ["entity", "t"],
+ ["entity_id", "t"],
+ ["before", "t"],
+ ["after", "t"],
+ ["actor", "t"],
+ ["created_at", "t"],
+ ],
+ { indexes: [["entity", "entity_id"]] },
+ );
+ await create(db, "match_suggestions", [
+ ["user_id", "fk:users"],
+ ["work_id", "fk:works"],
+ ["candidate_work_id", "fk:works"],
+ ["score", "r"],
+ ["status", "t"],
+ ["created_at", "t"],
+ ]);
+ await create(db, "source_runs", [
+ ["connection_id", "fk:source_connections"],
+ ["mode", "t"],
+ ["started_at", "t"],
+ ["finished_at", "t?"],
+ ["status", "t"],
+ ["counts", "t"],
+ ["log", "t"],
+ ["error", "t?"],
+ ]);
+ await create(
+ db,
+ "source_records",
+ [
+ ["connection_id", "fk:source_connections"],
+ ["key", "t"],
+ ["kind", "t"],
+ ["current_hash", "t"],
+ ["first_seen", "t"],
+ ["last_seen", "t"],
+ ["deleted_at", "t?"],
+ ["merged_into_key", "t?"],
+ ["target_entity", "t?"],
+ ["target_id", "t?"],
+ ],
+ { unique: [["connection_id", "key"]], indexes: [["target_entity", "target_id"]] },
+ );
+ await create(db, "source_record_versions", [
+ ["record_id", "fk:source_records"],
+ ["hash", "t"],
+ ["raw_gz", "t"],
+ ["seen_at", "t"],
+ ]);
+ await create(
+ db,
+ "change_events",
+ [
+ ["user_id", "fk:users"],
+ ["connection_id", "fk:source_connections?"],
+ ["run_id", "fk:source_runs?"],
+ ["record_id", "fk:source_records?"],
+ ["type", "t"],
+ ["target_entity", "t"],
+ ["target_id", "t?"],
+ ["payload", "t"],
+ ["status", "t"],
+ ["created_at", "t"],
+ ["resolved_at", "t?"],
+ ],
+ { indexes: [["user_id", "status"]] },
+ );
+ await create(
+ db,
+ "jobs",
+ [
+ ["type", "t"],
+ ["payload", "t"],
+ ["run_at", "t"],
+ ["status", "t"],
+ ["attempts", "i"],
+ ["last_error", "t?"],
+ ["locked_at", "t?"],
+ ["lock_key", "t?"],
+ ["created_at", "t"],
+ ],
+ { indexes: [["status", "run_at"], ["lock_key"]] },
+ );
+ },
+};
diff --git a/packages/db/src/migrations/index.ts b/packages/db/src/migrations/index.ts
index 418b1a7..c8c4ed7 100644
--- a/packages/db/src/migrations/index.ts
+++ b/packages/db/src/migrations/index.ts
@@ -1,4 +1,7 @@
import type { Migration } from "kysely/migration";
+import { initial } from "./0001_initial.ts";
/** All migrations, keyed by sortable name. Imported statically so bundles need no filesystem. */
-export const migrations: Record = {};
+export const migrations: Record = {
+ "0001_initial": initial,
+};
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index 2310ae7..f0be17e 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -1,2 +1,363 @@
-/** Kysely table types. Placeholder until the schema migration lands. */
-export interface Database {}
+/**
+ * Kysely table types. snake_case columns; timestamps are ISO-8601 text; JSON columns are text
+ * (use `toJson`/`fromJson`); booleans are integer 0/1. All IDs come from app code (`newId()`),
+ * so nothing here is `Generated<>`.
+ */
+
+export type Role = "admin" | "member";
+export type Visibility = "private" | "friends" | "public";
+export type TokenScope = "read" | "write" | "ingest";
+export type CustomFieldLevel = "entry" | "engagement" | "copy";
+export type ListKind = "list" | "grouping" | "thread";
+export type AttachmentEntity = "entry" | "engagement" | "copy" | "list" | "work";
+export type SuggestionStatus = "pending" | "accepted" | "rejected";
+export type ConnectionStatus = "active" | "paused" | "error";
+export type RunMode = "full" | "changes" | "file";
+export type RunStatus = "running" | "ok" | "failed";
+export type ChangeType = "added" | "changed" | "removed" | "merged" | "conflict";
+export type ChangeStatus = "pending" | "auto_applied" | "accepted" | "ignored" | "kept_mine";
+export type JobStatus = "queued" | "running" | "done" | "failed";
+
+export interface UsersTable {
+ id: string;
+ username: string;
+ display_name: string;
+ password_hash: string;
+ role: Role;
+ preferences: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface SessionsTable {
+ id: string;
+ user_id: string;
+ token_hash: string;
+ expires_at: string;
+ created_at: string;
+}
+
+export interface InvitesTable {
+ id: string;
+ code_hash: string;
+ created_by: string;
+ used_by: string | null;
+ expires_at: string;
+ created_at: string;
+}
+
+export interface AppTokensTable {
+ id: string;
+ user_id: string;
+ name: string;
+ token_hash: string;
+ /** JSON `TokenScope[]`. */
+ scopes: string;
+ last_used_at: string | null;
+ revoked_at: string | null;
+ created_at: string;
+}
+
+export interface WorksTable {
+ id: string;
+ media_type: string;
+ title: string;
+ original_title: string;
+ year: number | null;
+ release_date: string | null;
+ creators: string;
+ description: string;
+ cover_url: string;
+ cover_attachment_id: string | null;
+ metadata: string;
+ parent_id: string | null;
+ position: string | null;
+ owner_user_id: string | null;
+ merged_into_id: string | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ExternalIdsTable {
+ id: string;
+ work_id: string;
+ namespace: string;
+ value: string;
+}
+
+export interface WorkAliasesTable {
+ id: string;
+ user_id: string;
+ from_work_id: string;
+ to_work_id: string;
+}
+
+export interface WorkHistoryTable {
+ id: string;
+ work_id: string;
+ before: string;
+ after: string;
+ actor: string;
+ created_at: string;
+}
+
+export interface EntriesTable {
+ id: string;
+ user_id: string;
+ work_id: string;
+ status: string;
+ rating: number | null;
+ review: string | null;
+ private_notes: string | null;
+ /** 0 or 1. */
+ favorite: number;
+ tags: string;
+ custom_fields: string;
+ visibility: Visibility;
+ overrides: string;
+ media_type_override: string | null;
+ field_sources: string;
+ created_at: string;
+ updated_at: string;
+ deleted_at: string | null;
+}
+
+export interface EngagementsTable {
+ id: string;
+ entry_id: string;
+ started_at: string | null;
+ started_precision: string;
+ finished_at: string | null;
+ finished_precision: string;
+ progress: string;
+ platform: string | null;
+ rating: number | null;
+ notes: string | null;
+ custom_fields: string;
+ field_sources: string;
+ created_at: string;
+ updated_at: string;
+ deleted_at: string | null;
+}
+
+export interface EngagementSourcesTable {
+ id: string;
+ engagement_id: string;
+ connection_id: string;
+ record_key: string;
+}
+
+export interface ListensTable {
+ id: string;
+ user_id: string;
+ listened_at: string;
+ artist: string;
+ track: string;
+ album: string | null;
+ duration_ms: number | null;
+ mbids: string;
+ work_id: string | null;
+ source: string;
+ source_key: string;
+ created_at: string;
+}
+
+export interface ListsTable {
+ id: string;
+ user_id: string;
+ name: string;
+ description: string;
+ kind: ListKind;
+ visibility: Visibility;
+ rating: number | null;
+ review: string | null;
+ created_at: string;
+ updated_at: string;
+ deleted_at: string | null;
+}
+
+export interface ListItemsTable {
+ id: string;
+ list_id: string;
+ work_id: string;
+ position: number;
+ note: string | null;
+}
+
+export interface CustomFieldDefinitionsTable {
+ id: string;
+ user_id: string;
+ name: string;
+ type: string;
+ options: string;
+ /** JSON `MediaType[] | "all"`. */
+ applies_to: string;
+ level: CustomFieldLevel;
+ created_at: string;
+}
+
+export interface CopiesTable {
+ id: string;
+ user_id: string;
+ work_id: string | null;
+ title_hint: string | null;
+ format: string | null;
+ acquired_from: string | null;
+ acquired_at: string | null;
+ acquired_precision: string;
+ price_cents: number | null;
+ currency: string | null;
+ notes: string | null;
+ custom_fields: string;
+ source_ref: string | null;
+ created_at: string;
+ updated_at: string;
+ deleted_at: string | null;
+}
+
+export interface AttachmentsTable {
+ id: string;
+ user_id: string;
+ sha256: string;
+ mime: string;
+ size: number;
+ filename: string;
+ caption: string | null;
+ taken_at: string | null;
+ visibility: Visibility | null;
+ created_at: string;
+}
+
+export interface AttachmentLinksTable {
+ id: string;
+ attachment_id: string;
+ entity: AttachmentEntity;
+ entity_id: string;
+}
+
+export interface UserHistoryTable {
+ id: string;
+ user_id: string;
+ entity: string;
+ entity_id: string;
+ before: string;
+ after: string;
+ actor: string;
+ created_at: string;
+}
+
+export interface MatchSuggestionsTable {
+ id: string;
+ user_id: string;
+ work_id: string;
+ candidate_work_id: string;
+ score: number;
+ status: SuggestionStatus;
+ created_at: string;
+}
+
+export interface SourceConnectionsTable {
+ id: string;
+ user_id: string;
+ plugin_id: string;
+ name: string;
+ config: string;
+ credentials_enc: string | null;
+ schedule_minutes: number | null;
+ cursor: string | null;
+ status: ConnectionStatus;
+ last_run_at: string | null;
+ created_at: string;
+}
+
+export interface SourceRunsTable {
+ id: string;
+ connection_id: string;
+ mode: RunMode;
+ started_at: string;
+ finished_at: string | null;
+ status: RunStatus;
+ counts: string;
+ log: string;
+ error: string | null;
+}
+
+export interface SourceRecordsTable {
+ id: string;
+ connection_id: string;
+ key: string;
+ kind: string;
+ current_hash: string;
+ first_seen: string;
+ last_seen: string;
+ deleted_at: string | null;
+ merged_into_key: string | null;
+ target_entity: string | null;
+ target_id: string | null;
+}
+
+export interface SourceRecordVersionsTable {
+ id: string;
+ record_id: string;
+ hash: string;
+ /** Base64 of gzipped raw record. */
+ raw_gz: string;
+ seen_at: string;
+}
+
+export interface ChangeEventsTable {
+ id: string;
+ user_id: string;
+ connection_id: string | null;
+ run_id: string | null;
+ record_id: string | null;
+ type: ChangeType;
+ target_entity: string;
+ target_id: string | null;
+ payload: string;
+ status: ChangeStatus;
+ created_at: string;
+ resolved_at: string | null;
+}
+
+export interface JobsTable {
+ id: string;
+ type: string;
+ payload: string;
+ run_at: string;
+ status: JobStatus;
+ attempts: number;
+ last_error: string | null;
+ locked_at: string | null;
+ lock_key: string | null;
+ created_at: string;
+}
+
+export interface Database {
+ users: UsersTable;
+ sessions: SessionsTable;
+ invites: InvitesTable;
+ app_tokens: AppTokensTable;
+ works: WorksTable;
+ external_ids: ExternalIdsTable;
+ work_aliases: WorkAliasesTable;
+ work_history: WorkHistoryTable;
+ entries: EntriesTable;
+ engagements: EngagementsTable;
+ engagement_sources: EngagementSourcesTable;
+ listens: ListensTable;
+ lists: ListsTable;
+ list_items: ListItemsTable;
+ custom_field_definitions: CustomFieldDefinitionsTable;
+ copies: CopiesTable;
+ attachments: AttachmentsTable;
+ attachment_links: AttachmentLinksTable;
+ user_history: UserHistoryTable;
+ match_suggestions: MatchSuggestionsTable;
+ source_connections: SourceConnectionsTable;
+ source_runs: SourceRunsTable;
+ source_records: SourceRecordsTable;
+ source_record_versions: SourceRecordVersionsTable;
+ change_events: ChangeEventsTable;
+ jobs: JobsTable;
+}
diff --git a/packages/db/test/schema.test.ts b/packages/db/test/schema.test.ts
new file mode 100644
index 0000000..96dadd7
--- /dev/null
+++ b/packages/db/test/schema.test.ts
@@ -0,0 +1,78 @@
+import { expect, test } from "bun:test";
+import { newId, nowIso } from "../src/ids.ts";
+import { toJson } from "../src/json.ts";
+import { forEachDialect } from "../src/testing.ts";
+
+forEachDialect("schema", (getDb) => {
+ test("all tables exist and unique constraints hold", async () => {
+ const k = getDb().k;
+ const now = nowIso();
+ const userId = newId();
+ await k
+ .insertInto("users")
+ .values({
+ id: userId,
+ username: "ada",
+ display_name: "Ada",
+ password_hash: "x",
+ role: "admin",
+ preferences: toJson({}),
+ created_at: now,
+ updated_at: now,
+ })
+ .execute();
+
+ const workId = newId();
+ await k
+ .insertInto("works")
+ .values({
+ id: workId,
+ media_type: "book",
+ title: "Dune",
+ original_title: "Dune",
+ year: 1965,
+ release_date: null,
+ creators: toJson([]),
+ description: "",
+ cover_url: "",
+ cover_attachment_id: null,
+ metadata: toJson({}),
+ parent_id: null,
+ position: null,
+ owner_user_id: null,
+ merged_into_id: null,
+ created_at: now,
+ updated_at: now,
+ })
+ .execute();
+
+ await k
+ .insertInto("external_ids")
+ .values({ id: newId(), work_id: workId, namespace: "isbn", value: "123" })
+ .execute();
+ await expect(
+ k.insertInto("external_ids").values({ id: newId(), work_id: workId, namespace: "isbn", value: "123" }).execute(),
+ ).rejects.toThrow();
+
+ const entry = {
+ user_id: userId,
+ work_id: workId,
+ status: "planned",
+ rating: null,
+ review: null,
+ private_notes: null,
+ favorite: 0,
+ tags: toJson([]),
+ custom_fields: toJson({}),
+ visibility: "private" as const,
+ overrides: toJson({}),
+ media_type_override: null,
+ field_sources: toJson({}),
+ created_at: now,
+ updated_at: now,
+ deleted_at: null,
+ };
+ await k.insertInto("entries").values({ id: newId(), ...entry }).execute();
+ await expect(k.insertInto("entries").values({ id: newId(), ...entry }).execute()).rejects.toThrow();
+ });
+});