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:
25
Makefile
25
Makefile
@@ -7,13 +7,16 @@ PATCH_INCREMENT ?= 1
|
||||
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)
|
||||
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 \
|
||||
-X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \
|
||||
-X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \
|
||||
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
|
||||
-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
|
||||
|
||||
@@ -50,3 +53,23 @@ migrate:
|
||||
|
||||
clean:
|
||||
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
|
||||
|
||||
3996
migrations/020_generated_schema.sql
Normal file
3996
migrations/020_generated_schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
35
schema/README.md
Normal file
35
schema/README.md
Normal 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
44
schema/calendar.dbml
Normal 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
48
schema/core.dbml
Normal 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
53
schema/crm.dbml
Normal 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
21
schema/files.dbml
Normal 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
31
schema/household.dbml
Normal 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
30
schema/maintenance.dbml
Normal 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
49
schema/meals.dbml
Normal 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
29
schema/meta.dbml
Normal 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
42
schema/skills.dbml
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user