--- name: db description: Use when inspecting, debugging, or understanding the GOAT PostgreSQL database — querying projects, layers, users, orgs, teams, roles, jobs, or checking data state during local dev. --- # Database Query Query the GOAT PostgreSQL database to inspect data, debug issues, and understand state. ## Connection There is no host `psql`; go through the running Postgres container. The container name changes across setups (`goat-db`, `goat-db18`, …), so discover it rather than hardcoding: ```bash source /home/p4b/goat/.env DBC=$(docker ps --format '{{.Names}}' | grep -E '^goat-db' | head -1) docker exec -e PGPASSWORD=$POSTGRES_PASSWORD "$DBC" psql -h 127.0.0.1 -U $POSTGRES_USER -d $POSTGRES_DB ``` One-off query: ```bash source /home/p4b/goat/.env DBC=$(docker ps --format '{{.Names}}' | grep -E '^goat-db' | head -1) docker exec -e PGPASSWORD=$POSTGRES_PASSWORD "$DBC" psql -h 127.0.0.1 -U $POSTGRES_USER -d $POSTGRES_DB -c "YOUR SQL HERE" ``` ## Schemas | Schema | Purpose | |--------|---------| | `customer` | Everything: users, orgs, teams, roles/permissions, projects, layers, jobs, workflows | | `ducklake` | DuckLake catalog (managed by geoapi, don't modify directly) | ## Key Tables & Relationships The SQLModel definitions in `apps/core` are the source of truth — introspect when unsure: ```sql SELECT table_name FROM information_schema.tables WHERE table_schema='customer' ORDER BY 1; \d customer.layer ``` ### Identity & sharing (all in `customer`) - **user** (id uuid) — Keycloak-synced. firstname, lastname, avatar - **organization** (id uuid) — name, avatar; **organization_domain**, **organization_analytics** - **team** (id uuid) — belongs to org. name, avatar - **role** (id uuid) — permission roles; RBAC via **permission**, **role_permission**, **user_role**, **resource**, **resource_grant**, **resource_permission** - **user_team** — M2M user ↔ team; **invitation** — pending org/team invites - **layer_organization / layer_team / layer_user** — layer sharing with role - **project_organization / project_team / project_user** — project sharing with role ### Projects & layers (`customer`) - **project** (id uuid) — user_id, folder_id, layer_order[], basemap, tags[] - **layer** (id uuid) — user_id, folder_id. Key fields: name, type, data_type, tool_type, job_id, feature_layer_type, feature_layer_geometry_type, extent (geometry), properties (jsonb), other_properties (jsonb — holds `catalog_item` / `catalog_materialize` for promoted catalog layers), field_config (jsonb, per-column metadata), url, size, in_catalog, tags[], catalog_external_uid, catalog_version - **layer_project** (id int) — M2M layer ↔ project. name, properties (jsonb style config), other_properties, query (jsonb filters), charts, order, layer_project_group_id - **layer_project_group** (id int) — layer groups. project_id, parent_id (self-ref nesting), order - **folder** (id uuid) — user_id, name - **job** (id uuid) — user_id. type, status, payload (jsonb) - **workflow** (id uuid) — project_id, name, config (jsonb), is_default - **report** / **report_layout** (id uuid) — project_id, name, config (jsonb), is_default - **project_public** — public sharing config: password, config (jsonb snapshot) - **user_project** — user ↔ project with initial_view_state (jsonb) - **system_setting** — per-user: client_theme, preferred_language, unit - **uploaded_asset** — user uploads: s3_key, file_name, mime_type, file_size, asset_type, content_hash - **cost / credit_usage** — credit metering ## Common Queries ```sql -- Projects with layer counts SELECT p.id, p.name, p.created_at, COUNT(lp.id) AS layer_count FROM customer.project p LEFT JOIN customer.layer_project lp ON lp.project_id = p.id GROUP BY p.id ORDER BY p.created_at DESC; -- Layers in a project with styles SELECT lp.id, lp.name, lp.order, l.type, l.feature_layer_type, l.feature_layer_geometry_type FROM customer.layer_project lp JOIN customer.layer l ON l.id = lp.layer_id WHERE lp.project_id = 'PROJECT_UUID' ORDER BY lp.order; -- Job status SELECT id, type, status, created_at, payload->>'tool_type' AS tool FROM customer.job ORDER BY created_at DESC LIMIT 10; ``` ## Important Notes - Layer **metadata** lives in PostgreSQL (`customer.layer`), layer **data** lives in DuckLake (managed by geoapi) - `layer_project.properties` = style/rendering config (jsonb); `layer_project.query` = active filters (jsonb) - Scenarios are **gone** (2026-08-27): the feature, the three tables and `project.active_scenario_id` were all removed. `scenario_feature` was the last table using generic columns (`integer_attr1..25`); nothing in GOAT uses that scheme any more - A **catalog layer has no owner**: `layer.user_id` and `layer.folder_id` are NULL for promoted catalog layers. Joins from `layer` to `user` must be LEFT joins or those rows vanish - A public dashboard reads the `project_public.config` snapshot, not the live project — re-publish to reflect changes - Always use READ-ONLY queries. Never INSERT/UPDATE/DELETE unless explicitly asked - Use `ST_AsText()` or `ST_AsGeoJSON()` to read geometry columns