-
Notifications
You must be signed in to change notification settings - Fork 0
feat(habit): add recurring domain and immutable history #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
492c2f9
feat(habit): add recurring habit domain kernel
seonghobae 29b205d
test(habit): recover recurrence and immutable history behavior
seonghobae 6121031
feat(habit): add append-only recurring core schema
seonghobae a1cef78
docs(habit): document recurrence persistence guarantees
seonghobae 8c9b482
docs(habit): plan recurring domain slice
seonghobae 3d8fbb0
chore(format): gate recurring habit slice
seonghobae 12836b5
ci: render recurring habit formatting diagnostics
seonghobae b2bb920
fix(ci): preserve formatting gate integrity
seonghobae 28ec325
fix(habit): harden recurrence and tenant invariants
seonghobae 24d57cd
test(habit): cover week anchoring and strict persistence input
seonghobae 1e58dd8
ci: render habit test formatting diagnostics
seonghobae ed12462
style(habit): apply Prettier formatting
seonghobae d514665
ci: remove temporary habit diagnostics
seonghobae 5e9bd9b
chore(ci): diagnose habit test formatting
seonghobae 288ba08
ci: restore production validation workflow
seonghobae 81f62cf
fix(habit): harden composite keys and idempotency replay
seonghobae 783ec5c
test(habit): cover composite collisions and replay conflicts
seonghobae 047996e
fix(habit): reject truncation of completion history
seonghobae bea9047
docs(habit): document append-only database privileges
seonghobae 248da1d
perf(habit): index completion history ordering
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
112 changes: 112 additions & 0 deletions
112
apps/habit-service/migrations/0001_recurring_habit_core.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| BEGIN; | ||
|
|
||
| CREATE SCHEMA IF NOT EXISTS habit; | ||
|
|
||
| CREATE TABLE habit.habit_definitions ( | ||
| id uuid PRIMARY KEY, | ||
| workspace_id uuid NOT NULL, | ||
| title text NOT NULL, | ||
| timezone_name text NOT NULL, | ||
| recurrence_kind text NOT NULL, | ||
| recurrence_interval smallint NOT NULL, | ||
| weekday_mask smallint NOT NULL DEFAULT 0, | ||
| starts_on date NOT NULL, | ||
| created_at timestamptz NOT NULL, | ||
| CONSTRAINT habit_definitions_id_workspace_unique | ||
| UNIQUE (id, workspace_id), | ||
| CONSTRAINT habit_definitions_id_uuid_v4 CHECK ( | ||
| id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT habit_definitions_workspace_id_uuid_v4 CHECK ( | ||
| workspace_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT habit_definitions_title_nonblank CHECK ( | ||
| length(btrim(title)) > 0 | ||
| ), | ||
| CONSTRAINT habit_definitions_timezone_nonblank CHECK ( | ||
| length(btrim(timezone_name)) > 0 | ||
| ), | ||
| CONSTRAINT habit_definitions_recurrence_kind_valid CHECK ( | ||
| recurrence_kind IN ('daily', 'weekly') | ||
| ), | ||
| CONSTRAINT habit_definitions_recurrence_interval_valid CHECK ( | ||
| recurrence_interval BETWEEN 1 AND 365 | ||
| ), | ||
| CONSTRAINT habit_definitions_weekday_mask_valid CHECK ( | ||
| (recurrence_kind = 'daily' AND weekday_mask = 0) | ||
| OR | ||
| (recurrence_kind = 'weekly' AND weekday_mask BETWEEN 1 AND 127) | ||
| ) | ||
| ); | ||
|
|
||
| CREATE TABLE habit.completion_events ( | ||
| id uuid PRIMARY KEY, | ||
| workspace_id uuid NOT NULL, | ||
| habit_id uuid NOT NULL, | ||
| scheduled_local_date date NOT NULL, | ||
| completed_at timestamptz NOT NULL, | ||
| idempotency_key uuid NOT NULL, | ||
| recorded_at timestamptz NOT NULL, | ||
| CONSTRAINT completion_events_id_workspace_unique | ||
| UNIQUE (id, workspace_id), | ||
| CONSTRAINT completion_events_idempotency_unique | ||
| UNIQUE (workspace_id, habit_id, idempotency_key), | ||
| CONSTRAINT completion_events_habit_workspace_foreign | ||
| FOREIGN KEY (habit_id, workspace_id) | ||
| REFERENCES habit.habit_definitions (id, workspace_id), | ||
| CONSTRAINT completion_events_id_uuid_v4 CHECK ( | ||
| id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT completion_events_workspace_id_uuid_v4 CHECK ( | ||
| workspace_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT completion_events_habit_id_uuid_v4 CHECK ( | ||
| habit_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT completion_events_idempotency_key_uuid_v4 CHECK ( | ||
| idempotency_key::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ) | ||
| ); | ||
|
|
||
| CREATE INDEX habit_definitions_workspace_creation_idx | ||
| ON habit.habit_definitions (workspace_id, created_at ASC, id ASC); | ||
|
|
||
| CREATE INDEX completion_events_workspace_habit_schedule_idx | ||
| ON habit.completion_events ( | ||
| workspace_id, | ||
| habit_id, | ||
| scheduled_local_date ASC, | ||
| recorded_at ASC, | ||
| id ASC | ||
| ); | ||
|
|
||
| CREATE INDEX completion_events_workspace_habit_recorded_idx | ||
| ON habit.completion_events ( | ||
| workspace_id, | ||
| habit_id, | ||
| recorded_at ASC, | ||
| id ASC | ||
| ); | ||
|
|
||
| CREATE FUNCTION habit.reject_completion_mutation() | ||
| RETURNS trigger | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| BEGIN | ||
| RAISE EXCEPTION USING | ||
| ERRCODE = '55000', | ||
| MESSAGE = 'Habit completion history is append-only'; | ||
| END; | ||
| $$; | ||
|
|
||
| CREATE TRIGGER completion_events_append_only | ||
| BEFORE UPDATE OR DELETE ON habit.completion_events | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION habit.reject_completion_mutation(); | ||
|
|
||
| CREATE TRIGGER completion_events_reject_truncate | ||
| BEFORE TRUNCATE ON habit.completion_events | ||
| FOR EACH STATEMENT | ||
| EXECUTE FUNCTION habit.reject_completion_mutation(); | ||
|
|
||
| COMMIT; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Habit migrations | ||
|
|
||
| Apply Habit SQL files in lexical order to the PostgreSQL database owned by the Habit service before starting the corresponding application version. | ||
|
|
||
| - `0001_recurring_habit_core.sql` creates tenant-safe habit definitions and append-only completion events. Weekly recurrence days are stored as a seven-bit ISO-weekday mask, while the service domain exposes normalized weekday numbers from Monday (`1`) through Sunday (`7`). | ||
|
|
||
| ## Integrity guarantees | ||
|
|
||
| Every persisted entity, workspace, habit reference, event, and idempotency key is constrained to UUIDv4. Composite foreign keys carry `workspace_id` through the ownership path, and duplicate completion commands are identified by `(workspace_id, habit_id, idempotency_key)`. | ||
|
|
||
| Database triggers reject `UPDATE`, `DELETE`, and `TRUNCATE` operations on completion events. The service runtime role must not receive `UPDATE`, `DELETE`, or `TRUNCATE` privileges on `habit.completion_events`. A future data-rights migration must provide a separately authorized erasure path before production account deletion is enabled; application code must not bypass append-only history through direct SQL. | ||
|
|
||
| ## Rollback | ||
|
|
||
| This migration is forward-only in automated environments. An operator-approved rollback must first export tenant data, then drop `habit.completion_events`, `habit.habit_definitions`, `habit.reject_completion_mutation()`, and the `habit` schema. Do not roll back after serving completion writes unless the exported history has been verified and the data-retention decision is documented. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.