feat: add DBML schema files and relspecgo migration generation

- Add schema/*.dbml covering all existing tables (001-019)
- Wire relspecgo via make generate-migrations target
- Add make check-schema-drift for CI drift detection
- Add schema/README.md documenting the DBML-first workflow

Closes #19
This commit is contained in:
2026-04-04 14:53:33 +02:00
parent f0e242293f
commit 59c43188e5
12 changed files with 4402 additions and 1 deletions

View File

@@ -7,13 +7,16 @@ PATCH_INCREMENT ?= 1
VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev) VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
RELSPEC ?= $(shell command -v relspec 2>/dev/null || echo $(HOME)/go/bin/relspec)
SCHEMA_FILES := $(sort $(wildcard schema/*.dbml))
GENERATED_SCHEMA_MIGRATION := migrations/020_generated_schema.sql
LDFLAGS := -s -w \ LDFLAGS := -s -w \
-X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \ -X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \
-X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \ -X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \ -X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE) -X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
.PHONY: all build clean migrate release-version test .PHONY: all build clean migrate release-version test generate-migrations check-schema-drift
all: build all: build
@@ -50,3 +53,23 @@ migrate:
clean: clean:
rm -rf $(BIN_DIR) rm -rf $(BIN_DIR)
generate-migrations:
@test -n "$(SCHEMA_FILES)" || (echo "No DBML schema files found in schema/" >&2; exit 1)
@command -v $(RELSPEC) >/dev/null 2>&1 || (echo "relspec not found; install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest" >&2; exit 1)
@schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
$(RELSPEC) convert --from dbml --from-list "$$schema_list" --to pgsql --to-path $(GENERATED_SCHEMA_MIGRATION)
check-schema-drift:
@test -f $(GENERATED_SCHEMA_MIGRATION) || (echo "$(GENERATED_SCHEMA_MIGRATION) is missing; run make generate-migrations" >&2; exit 1)
@command -v $(RELSPEC) >/dev/null 2>&1 || (echo "relspec not found; install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest" >&2; exit 1)
@tmpfile=$$(mktemp); \
schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
$(RELSPEC) convert --from dbml --from-list "$$schema_list" --to pgsql --to-path $$tmpfile; \
if ! cmp -s $$tmpfile $(GENERATED_SCHEMA_MIGRATION); then \
echo "Schema drift detected between schema/*.dbml and $(GENERATED_SCHEMA_MIGRATION)" >&2; \
diff -u $(GENERATED_SCHEMA_MIGRATION) $$tmpfile || true; \
rm -f $$tmpfile; \
exit 1; \
fi; \
rm -f $$tmpfile

File diff suppressed because it is too large Load Diff

35
schema/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Schema workflow
The `schema/*.dbml` files are the database schema source of truth.
## Generate SQL migrations
Run:
```bash
make generate-migrations
```
This uses `relspec` to convert the DBML files into PostgreSQL SQL and writes the generated schema migration to:
- `migrations/020_generated_schema.sql`
## Check schema drift
Run:
```bash
make check-schema-drift
```
This regenerates the SQL from `schema/*.dbml` and compares it with `migrations/020_generated_schema.sql`.
If the generated output differs, the command fails so CI can catch schema drift.
## Workflow
1. Update the DBML files in `schema/`
2. Run `make generate-migrations`
3. Review the generated SQL
4. Commit both the DBML changes and the generated migration
Existing handwritten migrations stay in place. Going forward, update the DBML first and regenerate the SQL from there.

44
schema/calendar.dbml Normal file
View File

@@ -0,0 +1,44 @@
Table family_members {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
relationship text
birth_date date
notes text
created_at timestamptz [not null, default: `now()`]
}
Table activities {
id uuid [pk, default: `gen_random_uuid()`]
family_member_id uuid [ref: > family_members.id]
title text [not null]
activity_type text
day_of_week text
start_time time
end_time time
start_date date
end_date date
location text
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
day_of_week
family_member_id
(start_date, end_date)
}
}
Table important_dates {
id uuid [pk, default: `gen_random_uuid()`]
family_member_id uuid [ref: > family_members.id]
title text [not null]
date_value date [not null]
recurring_yearly boolean [not null, default: false]
reminder_days_before int [not null, default: 7]
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
date_value
}
}

48
schema/core.dbml Normal file
View File

@@ -0,0 +1,48 @@
Table thoughts {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
content text [not null]
metadata jsonb [default: `'{}'::jsonb`]
created_at timestamptz [default: `now()`]
updated_at timestamptz [default: `now()`]
project_id uuid [ref: > projects.guid]
archived_at timestamptz
}
Table projects {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
name text [unique, not null]
description text
created_at timestamptz [default: `now()`]
last_active_at timestamptz [default: `now()`]
}
Table thought_links {
from_id bigint [not null, ref: > thoughts.id]
to_id bigint [not null, ref: > thoughts.id]
relation text [not null]
created_at timestamptz [default: `now()`]
indexes {
(from_id, to_id, relation) [pk]
from_id
to_id
}
}
Table embeddings {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
thought_id uuid [not null, ref: > thoughts.guid]
model text [not null]
dim int [not null]
embedding vector [not null]
created_at timestamptz [default: `now()`]
updated_at timestamptz [default: `now()`]
indexes {
(thought_id, model) [unique]
thought_id
}
}

53
schema/crm.dbml Normal file
View File

@@ -0,0 +1,53 @@
Table professional_contacts {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
company text
title text
email text
phone text
linkedin_url text
how_we_met text
tags "text[]" [not null, default: `'{}'`]
notes text
last_contacted timestamptz
follow_up_date date
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
last_contacted
follow_up_date
}
}
Table contact_interactions {
id uuid [pk, default: `gen_random_uuid()`]
contact_id uuid [not null, ref: > professional_contacts.id]
interaction_type text [not null]
occurred_at timestamptz [not null, default: `now()`]
summary text [not null]
follow_up_needed boolean [not null, default: false]
follow_up_notes text
created_at timestamptz [not null, default: `now()`]
indexes {
(contact_id, occurred_at)
}
}
Table opportunities {
id uuid [pk, default: `gen_random_uuid()`]
contact_id uuid [ref: > professional_contacts.id]
title text [not null]
description text
stage text [not null, default: 'identified']
value "decimal(12,2)"
expected_close_date date
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
stage
}
}

21
schema/files.dbml Normal file
View File

@@ -0,0 +1,21 @@
Table stored_files {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
thought_id uuid [ref: > thoughts.guid]
project_id uuid [ref: > projects.guid]
name text [not null]
media_type text [not null]
kind text [not null, default: 'file']
encoding text [not null, default: 'base64']
size_bytes bigint [not null]
sha256 text [not null]
content bytea [not null]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
thought_id
project_id
sha256
}
}

31
schema/household.dbml Normal file
View File

@@ -0,0 +1,31 @@
Table household_items {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
category text
location text
details jsonb [not null, default: `'{}'`]
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
category
}
}
Table household_vendors {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
service_type text
phone text
email text
website text
notes text
rating int
last_used date
created_at timestamptz [not null, default: `now()`]
indexes {
service_type
}
}

30
schema/maintenance.dbml Normal file
View File

@@ -0,0 +1,30 @@
Table maintenance_tasks {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
category text
frequency_days int
last_completed timestamptz
next_due timestamptz
priority text [not null, default: 'medium']
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
next_due
}
}
Table maintenance_logs {
id uuid [pk, default: `gen_random_uuid()`]
task_id uuid [not null, ref: > maintenance_tasks.id]
completed_at timestamptz [not null, default: `now()`]
performed_by text
cost "decimal(10,2)"
notes text
next_action text
indexes {
(task_id, completed_at)
}
}

49
schema/meals.dbml Normal file
View File

@@ -0,0 +1,49 @@
Table recipes {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
cuisine text
prep_time_minutes int
cook_time_minutes int
servings int
ingredients jsonb [not null, default: `'[]'`]
instructions jsonb [not null, default: `'[]'`]
tags "text[]" [not null, default: `'{}'`]
rating int
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
cuisine
tags
}
}
Table meal_plans {
id uuid [pk, default: `gen_random_uuid()`]
week_start date [not null]
day_of_week text [not null]
meal_type text [not null]
recipe_id uuid [ref: > recipes.id]
custom_meal text
servings int
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
week_start
}
}
Table shopping_lists {
id uuid [pk, default: `gen_random_uuid()`]
week_start date [unique, not null]
items jsonb [not null, default: `'[]'`]
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
week_start
}
}

29
schema/meta.dbml Normal file
View File

@@ -0,0 +1,29 @@
Table chat_histories {
id uuid [pk, default: `gen_random_uuid()`]
session_id text [not null]
title text
channel text
agent_id text
project_id uuid [ref: > projects.guid]
messages jsonb [not null, default: `'[]'`]
summary text
metadata jsonb [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
session_id
project_id
channel
agent_id
created_at
}
}
Table tool_annotations {
id bigserial [pk]
tool_name text [unique, not null]
notes text [not null, default: '']
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}

42
schema/skills.dbml Normal file
View File

@@ -0,0 +1,42 @@
Table agent_skills {
id uuid [pk, default: `gen_random_uuid()`]
name text [unique, not null]
description text [not null, default: '']
content text [not null]
tags "text[]" [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}
Table agent_guardrails {
id uuid [pk, default: `gen_random_uuid()`]
name text [unique, not null]
description text [not null, default: '']
content text [not null]
severity text [not null, default: 'medium']
tags "text[]" [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}
Table project_skills {
project_id uuid [not null, ref: > projects.guid]
skill_id uuid [not null, ref: > agent_skills.id]
created_at timestamptz [not null, default: `now()`]
indexes {
(project_id, skill_id) [pk]
project_id
}
}
Table project_guardrails {
project_id uuid [not null, ref: > projects.guid]
guardrail_id uuid [not null, ref: > agent_guardrails.id]
created_at timestamptz [not null, default: `now()`]
indexes {
(project_id, guardrail_id) [pk]
project_id
}
}