diff --git a/PULSE_DATABASE_SKILL.md b/PULSE_DATABASE_SKILL.md index 4b99e28..b4de727 100644 --- a/PULSE_DATABASE_SKILL.md +++ b/PULSE_DATABASE_SKILL.md @@ -16,7 +16,7 @@ | Domain | Key Tables | Approx Rows | Description | |---|---|---|---| -| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs | Service desk, billing, contracts, clients | +| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, project_phases, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs, 2.7K phases | Service desk, billing, contracts, clients | | **Datto RMM** | datto_rmm_alerts, datto_rmm_devices, datto_rmm_sites | 21K alerts, 3.6K devices | Remote monitoring & management | | **Auvik** | auvik_tenants, auvik_tenant_mappings | 17 tenants, 15 mappings | Network management — device inventory fetched live from API; tenant/company links stored in DB | | **SentinelOne** | s1_agents, s1_threats, s1_sites | 2.8K agents, 4.1K threats | Endpoint security | @@ -222,6 +222,35 @@ Planned engagements for client work — distinct from reactive service tickets. **Relationship to tickets:** A ticket can optionally be linked to a project via `tickets.project_id`. This is typically used when a ticket was generated as part of project work (e.g. a change request within a project). +### project_phases (~2.7K rows, 19 columns) + +Phases group tasks within a project (e.g. "Equipment Selection", "Installation", "Testing"). Every phase belongs to exactly one project. + +**Key columns:** +- `id` (bigint PK) — Autotask phase ID (shares namespace with tasks) +- `project_id` (bigint) — FK to `projects.id` (no FK constraint — may reference archived projects) +- `parent_phase_id` (bigint) — parent phase for nested phase trees +- `title` (varchar) — phase name +- `description` (text) +- `phase_number` (varchar) — e.g. `T20251222.0116` +- `estimated_hours` (numeric) +- `start_date_time`, `due_date` (timestamp) — scheduled window +- `create_date_time`, `last_activity_date_time` (timestamp) +- `is_scheduled` (boolean) +- `creator_resource_id` (bigint) — FK to `resources.id` +- `is_deleted` (boolean) + +**Why this table matters:** The Autotask Tasks bulk API omits `projectID` from its response. After each `project_phases` sync, an automatic backfill updates `tasks.project_id` by joining `tasks.phase_id = project_phases.id`. This is the only way to resolve which project a task belongs to. + +**Join to get a task's project via phase:** +```sql +SELECT t.id, t.title, pp.title AS phase, p.project_name +FROM tasks t +JOIN project_phases pp ON pp.id = t.phase_id +LEFT JOIN projects p ON p.id = pp.project_id +WHERE t.is_deleted IS NOT TRUE; +``` + ### tasks (~5K rows, 34 columns) Tasks are discrete units of work that live **inside** a project or a ticket. They are NOT standalone items — every task belongs to either a project (`project_id`) or a ticket (`ticket_id`), not both simultaneously. @@ -254,13 +283,21 @@ Tasks are discrete units of work that live **inside** a project or a ticket. The **Time entries on tasks:** `time_entries.task_id` links work logs to specific tasks. Task time entries also carry `project_id` and `company_id` for quick aggregation without joins. +**Known sync behaviour — `project_id` population:** +The Autotask bulk Tasks API does not return `projectID` in its response. `project_id` is instead populated post-sync via a backfill that joins `tasks.phase_id → project_phases.id → project_phases.project_id`. This backfill runs automatically every time `project_phases` is synced. Tasks whose parent project is completed/archived (and therefore absent from our `projects` table) will still have `project_id = NULL` — use `phase_id → project_phases.project_id` directly in those cases. + **Finding project tasks vs ticket tasks:** ```sql --- Project tasks only +-- Project tasks only (project_id populated via phase backfill) SELECT t.*, p.project_name FROM tasks t JOIN projects p ON p.id = t.project_id WHERE t.project_id IS NOT NULL AND t.is_deleted IS NOT TRUE; +-- Tasks whose project is archived (project_id NULL but phase_id resolvable) +SELECT t.*, pp.project_id AS phase_project_id, pp.title AS phase_title +FROM tasks t JOIN project_phases pp ON pp.id = t.phase_id +WHERE t.project_id IS NULL AND t.is_deleted IS NOT TRUE; + -- Ticket sub-tasks only SELECT t.*, tk.title AS ticket_title FROM tasks t JOIN tickets tk ON tk.id = t.ticket_id