feat: DBML schema files + relspecgo migration generation #20
30
Makefile
30
Makefile
@@ -7,13 +7,17 @@ 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))
|
||||||
|
MERGE_TARGET_TMP := $(CURDIR)/.cache/schema.merge-target.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 +54,27 @@ 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)
|
||||||
|
@mkdir -p $(dir $(MERGE_TARGET_TMP))
|
||||||
|
@: > $(MERGE_TARGET_TMP)
|
||||||
|
@schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
|
||||||
|
$(RELSPEC) merge --target dbml --target-path $(MERGE_TARGET_TMP) --source dbml --from-list "$$schema_list" --output pgsql --output-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)
|
||||||
|
@mkdir -p $(dir $(MERGE_TARGET_TMP))
|
||||||
|
@tmpfile=$$(mktemp); \
|
||||||
|
: > $(MERGE_TARGET_TMP); \
|
||||||
|
schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
|
||||||
|
$(RELSPEC) merge --target dbml --target-path $(MERGE_TARGET_TMP) --source dbml --from-list "$$schema_list" --output pgsql --output-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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
schema/files.dbml
Normal file
25
schema/files.dbml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-file refs (for relspecgo merge)
|
||||||
|
Ref: stored_files.thought_id > thoughts.guid [delete: set null]
|
||||||
|
Ref: stored_files.project_id > projects.guid [delete: set null]
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
32
schema/meta.dbml
Normal file
32
schema/meta.dbml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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()`]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-file refs (for relspecgo merge)
|
||||||
|
Ref: chat_histories.project_id > projects.guid [delete: set null]
|
||||||
46
schema/skills.dbml
Normal file
46
schema/skills.dbml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-file refs (for relspecgo merge)
|
||||||
|
Ref: project_skills.project_id > projects.guid [delete: cascade]
|
||||||
|
Ref: project_guardrails.project_id > projects.guid [delete: cascade]
|
||||||
Reference in New Issue
Block a user