CREATE SCHEMA "tasks"; --> statement-breakpoint CREATE TABLE "tasks"."activity" ( "id" serial PRIMARY KEY, "task_id" integer, "goal_id" integer, "kind" text NOT NULL, "from_status" text, "to_status" text, "note" text, "value" numeric(14,2), "created_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "activity_target_check" CHECK ("task_id" IS NOT NULL OR "goal_id" IS NOT NULL), CONSTRAINT "activity_kind_check" CHECK ("kind" IN ('created','status','edit','note','progress')) ); --> statement-breakpoint CREATE TABLE "tasks"."areas" ( "id" serial PRIMARY KEY, "slug" text NOT NULL, "name" text NOT NULL, "description" text, "color" text, "sort_order" integer DEFAULT 0 NOT NULL, "archived_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "areas_slug_check" CHECK ("slug" ~ '^[a-z0-9][a-z0-9-]{0,62}$'), CONSTRAINT "areas_name_check" CHECK (length(btrim("name")) > 0) ); --> statement-breakpoint CREATE TABLE "tasks"."goals" ( "id" serial PRIMARY KEY, "title" text NOT NULL, "notes" text, "status" text DEFAULT 'active' NOT NULL, "area_id" integer, "target_date" date, "metric" text, "metric_unit" text, "metric_target" numeric(14,2), "metric_current" numeric(14,2), "flagged" boolean DEFAULT false NOT NULL, "sort_order" integer DEFAULT 0 NOT NULL, "source" text, "achieved_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "goals_title_check" CHECK (length(btrim("title")) > 0), CONSTRAINT "goals_status_check" CHECK ("status" IN ('active','achieved','dropped')) ); --> statement-breakpoint CREATE TABLE "tasks"."tasks" ( "id" serial PRIMARY KEY, "title" text NOT NULL, "notes" text, "status" text DEFAULT 'todo' NOT NULL, "priority" smallint DEFAULT 0 NOT NULL, "flagged" boolean DEFAULT false NOT NULL, "due_date" date, "due_time" time, "reminder_at" timestamp with time zone, "recurrence" text, "area_id" integer, "goal_id" integer, "parent_id" integer, "tags" text[] DEFAULT '{}'::text[] NOT NULL, "sort_order" integer DEFAULT 0 NOT NULL, "source" text, "external_id" text, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "tasks_title_check" CHECK (length(btrim("title")) > 0), CONSTRAINT "tasks_status_check" CHECK ("status" IN ('todo','in_progress','done','dropped')), CONSTRAINT "tasks_priority_check" CHECK ("priority" BETWEEN 0 AND 3), CONSTRAINT "tasks_parent_check" CHECK ("parent_id" IS NULL OR "parent_id" <> "id"), CONSTRAINT "tasks_time_check" CHECK ("due_time" IS NULL OR "due_date" IS NOT NULL) ); --> statement-breakpoint CREATE INDEX "activity_task_idx" ON "tasks"."activity" ("task_id","created_at");--> statement-breakpoint CREATE INDEX "activity_goal_idx" ON "tasks"."activity" ("goal_id","created_at");--> statement-breakpoint CREATE UNIQUE INDEX "areas_slug_key" ON "tasks"."areas" ("slug");--> statement-breakpoint CREATE UNIQUE INDEX "goals_title_key" ON "tasks"."goals" (lower("title"));--> statement-breakpoint CREATE INDEX "tasks_area_idx" ON "tasks"."tasks" ("area_id");--> statement-breakpoint CREATE INDEX "tasks_goal_idx" ON "tasks"."tasks" ("goal_id");--> statement-breakpoint CREATE INDEX "tasks_parent_idx" ON "tasks"."tasks" ("parent_id");--> statement-breakpoint CREATE INDEX "tasks_status_due_idx" ON "tasks"."tasks" ("status","due_date");--> statement-breakpoint CREATE INDEX "tasks_tags_idx" ON "tasks"."tasks" USING gin ("tags");--> statement-breakpoint CREATE UNIQUE INDEX "tasks_source_external_key" ON "tasks"."tasks" ("source","external_id") WHERE "external_id" IS NOT NULL;--> statement-breakpoint ALTER TABLE "tasks"."activity" ADD CONSTRAINT "activity_task_id_tasks_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"."tasks"("id") ON DELETE CASCADE;--> statement-breakpoint ALTER TABLE "tasks"."activity" ADD CONSTRAINT "activity_goal_id_goals_id_fkey" FOREIGN KEY ("goal_id") REFERENCES "tasks"."goals"("id") ON DELETE CASCADE;--> statement-breakpoint ALTER TABLE "tasks"."goals" ADD CONSTRAINT "goals_area_id_areas_id_fkey" FOREIGN KEY ("area_id") REFERENCES "tasks"."areas"("id") ON DELETE SET NULL;--> statement-breakpoint ALTER TABLE "tasks"."tasks" ADD CONSTRAINT "tasks_area_id_areas_id_fkey" FOREIGN KEY ("area_id") REFERENCES "tasks"."areas"("id") ON DELETE SET NULL;--> statement-breakpoint ALTER TABLE "tasks"."tasks" ADD CONSTRAINT "tasks_goal_id_goals_id_fkey" FOREIGN KEY ("goal_id") REFERENCES "tasks"."goals"("id") ON DELETE SET NULL;--> statement-breakpoint ALTER TABLE "tasks"."tasks" ADD CONSTRAINT "tasks_parent_id_tasks_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "tasks"."tasks"("id") ON DELETE CASCADE; --> statement-breakpoint -- Hand-written below this line (not generated by drizzle-kit). -- updated_at triggers, same as every other core table. DO $$ DECLARE t text; BEGIN IF to_regprocedure('meta.touch_updated_at()') IS NOT NULL THEN FOREACH t IN ARRAY ARRAY['areas','goals','tasks'] LOOP EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON tasks.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t); END LOOP; END IF; END $$; --> statement-breakpoint -- Privacy: like health, the task tracker is readable/writable by the app role only -- (no personal_ro grant). Grants only run on prod, where migrations run as `personal`. REVOKE ALL ON SCHEMA tasks FROM PUBLIC; --> statement-breakpoint DO $$ BEGIN IF current_user = 'personal' AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN GRANT USAGE ON SCHEMA tasks TO personal_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tasks TO personal_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA tasks TO personal_app; ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA tasks GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app; ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA tasks GRANT USAGE, SELECT ON SEQUENCES TO personal_app; END IF; END $$; --> statement-breakpoint -- Registered in meta.tables (structure only; the lists GUI only shows kind = 'list'). DO $$ BEGIN IF to_regclass('meta.tables') IS NOT NULL THEN INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order) VALUES ('tasks','tasks','tasks-tasks','Tasks','core',true,true,1000), ('tasks','goals','tasks-goals','Goals','core',true,false,1000), ('tasks','areas','tasks-areas','Task areas','core',true,false,1000), ('tasks','activity','tasks-activity','Task activity','core',false,false,1000) ON CONFLICT DO NOTHING; -- deploy-remote.sh runs meta.sync() as the app role after migrating. IF has_function_privilege('meta.sync(integer)', 'EXECUTE') THEN PERFORM meta.sync(); END IF; END IF; END $$;