plain.postgres 0.85.0__tar.gz → 0.86.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/PKG-INFO +1 -1
  2. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/CHANGELOG.md +12 -0
  3. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +4 -0
  4. plain_postgres-0.86.0/plain/postgres/agents/.claude/skills/plain-postgres-diagnose/SKILL.md +76 -0
  5. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/cli/db.py +2 -0
  6. plain_postgres-0.86.0/plain/postgres/cli/diagnose.py +896 -0
  7. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/preflight.py +107 -0
  8. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/pyproject.toml +1 -1
  9. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/.gitignore +0 -0
  10. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/CLAUDE.md +0 -0
  11. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/LICENSE +0 -0
  12. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/README.md +0 -0
  13. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/README.md +0 -0
  14. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/__init__.py +0 -0
  15. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/aggregates.py +0 -0
  16. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/backups/__init__.py +0 -0
  17. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/backups/cli.py +0 -0
  18. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/backups/clients.py +0 -0
  19. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/backups/core.py +0 -0
  20. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/base.py +0 -0
  21. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/cli/__init__.py +0 -0
  22. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/cli/migrations.py +0 -0
  23. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/config.py +0 -0
  24. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/connection.py +0 -0
  25. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/connections.py +0 -0
  26. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/constants.py +0 -0
  27. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/constraints.py +0 -0
  28. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/database_url.py +0 -0
  29. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/db.py +0 -0
  30. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/default_settings.py +0 -0
  31. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/deletion.py +0 -0
  32. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/dialect.py +0 -0
  33. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/entrypoints.py +0 -0
  34. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/enums.py +0 -0
  35. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/exceptions.py +0 -0
  36. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/expressions.py +0 -0
  37. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/__init__.py +0 -0
  38. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/encrypted.py +0 -0
  39. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/json.py +0 -0
  40. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/mixins.py +0 -0
  41. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/related.py +0 -0
  42. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/related_descriptors.py +0 -0
  43. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/related_lookups.py +0 -0
  44. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/related_managers.py +0 -0
  45. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  46. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/reverse_related.py +0 -0
  47. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/fields/timezones.py +0 -0
  48. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/forms.py +0 -0
  49. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/__init__.py +0 -0
  50. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/comparison.py +0 -0
  51. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/datetime.py +0 -0
  52. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/math.py +0 -0
  53. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/mixins.py +0 -0
  54. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/text.py +0 -0
  55. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/functions/window.py +0 -0
  56. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/indexes.py +0 -0
  57. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/lookups.py +0 -0
  58. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/meta.py +0 -0
  59. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/__init__.py +0 -0
  60. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/autodetector.py +0 -0
  61. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/exceptions.py +0 -0
  62. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/executor.py +0 -0
  63. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/graph.py +0 -0
  64. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/loader.py +0 -0
  65. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/migration.py +0 -0
  66. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  67. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/operations/base.py +0 -0
  68. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/operations/fields.py +0 -0
  69. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/operations/models.py +0 -0
  70. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/operations/special.py +0 -0
  71. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/optimizer.py +0 -0
  72. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/questioner.py +0 -0
  73. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/recorder.py +0 -0
  74. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/serializer.py +0 -0
  75. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/state.py +0 -0
  76. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/utils.py +0 -0
  77. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/migrations/writer.py +0 -0
  78. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/options.py +0 -0
  79. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/otel.py +0 -0
  80. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/query.py +0 -0
  81. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/query_utils.py +0 -0
  82. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/registry.py +0 -0
  83. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/schema.py +0 -0
  84. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/__init__.py +0 -0
  85. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/compiler.py +0 -0
  86. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/constants.py +0 -0
  87. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/datastructures.py +0 -0
  88. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/query.py +0 -0
  89. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/sql/where.py +0 -0
  90. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/test/__init__.py +0 -0
  91. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/test/pytest.py +0 -0
  92. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/test/utils.py +0 -0
  93. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/transaction.py +0 -0
  94. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/types.py +0 -0
  95. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/types.pyi +0 -0
  96. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/plain/postgres/utils.py +0 -0
  97. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  98. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  99. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  100. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  101. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  102. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  103. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/migrations/__init__.py +0 -0
  104. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/examples/models.py +0 -0
  105. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/settings.py +0 -0
  106. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/app/urls.py +0 -0
  107. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_connection_isolation.py +0 -0
  108. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_connection_lifecycle.py +0 -0
  109. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_database_url.py +0 -0
  110. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_delete_behaviors.py +0 -0
  111. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_encrypted_fields.py +0 -0
  112. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_exceptions.py +0 -0
  113. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_iterator.py +0 -0
  114. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_manager_assignment.py +0 -0
  115. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_models.py +0 -0
  116. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_read_only_transactions.py +0 -0
  117. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_related_descriptors.py +0 -0
  118. {plain_postgres-0.85.0 → plain_postgres-0.86.0}/tests/test_related_manager_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.85.0
3
+ Version: 0.86.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -1,5 +1,17 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.86.0](https://github.com/dropseed/plain/releases/plain-postgres@0.86.0) (2026-03-24)
4
+
5
+ ### What's changed
6
+
7
+ - **New `plain db diagnose` command** — runs health checks against your Postgres database and reports issues as structured JSON. Checks for unused indexes, duplicate indexes, missing foreign key indexes, sequence exhaustion, transaction ID wraparound, vacuum health, and slow queries (via `pg_stat_statements`). Each finding includes table ownership info (app vs package) and actionable suggestions ([91994604b60d](https://github.com/dropseed/plain/commit/91994604b60d))
8
+ - **New preflight checks** for missing foreign key indexes and duplicate indexes — these run automatically during `plain check` and flag issues before they hit production ([3703fe8ab38d](https://github.com/dropseed/plain/commit/3703fe8ab38d))
9
+ - New `plain-postgres-diagnose` AI skill for guided database health check workflow ([91994604b60d](https://github.com/dropseed/plain/commit/91994604b60d))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - No changes required.
14
+
3
15
  ## [0.85.0](https://github.com/dropseed/plain/releases/plain-postgres@0.85.0) (2026-03-22)
4
16
 
5
17
  ### What's changed
@@ -68,6 +68,10 @@ Run `uv run plain docs postgres --section querying` for full patterns with code
68
68
 
69
69
  Run `uv run plain docs postgres --section constraints` for full patterns with code examples.
70
70
 
71
+ ## Diagnostics
72
+
73
+ Use the `/plain-postgres-diagnose` skill to run database health checks (unused indexes, missing FK indexes, sequence exhaustion, etc.).
74
+
71
75
  ## Differences from Django
72
76
 
73
77
  - Use `Model.query` not `Model.objects`
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: plain-postgres-diagnose
3
+ description: Run Postgres health checks and fix issues. Use when asked to check database health, optimize the database, find unused indexes, or diagnose Postgres problems.
4
+ ---
5
+
6
+ # Database Diagnostics
7
+
8
+ Run health checks against the production Postgres database and act on findings locally.
9
+
10
+ ## 1. Determine how to run the command
11
+
12
+ The diagnose command should run against the **production database**, not the local dev database. Ask the user how they run commands in production if you don't know. Common patterns:
13
+
14
+ - **Heroku**: `heroku run uv run plain db diagnose --json -a app-name`
15
+ - **Direct**: `uv run plain db diagnose --json` (if DATABASE_URL points to production)
16
+ - **Other platforms**: wrap with the platform's run command
17
+
18
+ For local/dev analysis, run directly:
19
+
20
+ ```
21
+ uv run plain db diagnose --json
22
+ ```
23
+
24
+ ## 2. Interpret the JSON output
25
+
26
+ The output has two sections:
27
+
28
+ **`checks`** — an array of health check results, each with:
29
+
30
+ - `name` — check identifier
31
+ - `status` — `ok`, `warning`, `critical`, `skipped`, or `error`
32
+ - `items` — specific findings, each with:
33
+ - `table`, `name`, `detail` — what was found
34
+ - `source` — `"app"` (user's code), `"package"` (framework/third-party dependency), or `""` (unmanaged)
35
+ - `package` — the package label (e.g., `"jobs"`, `"observer"`) if source is `"package"`
36
+ - `suggestion` — what to do about it
37
+ - `message` — additional context
38
+
39
+ **`context`** — supporting information:
40
+
41
+ - `tables` — all tables with row counts, sizes, index counts, `source`, and `package`
42
+ - `connections` — active vs max connections
43
+ - `stats_reset` — when pg_stat_user_indexes was last reset (affects "unused" interpretation)
44
+ - `pg_stat_statements` — whether query analysis is available
45
+ - `slow_queries` — top 10 queries by total execution time (if available)
46
+
47
+ ## 3. Fix issues
48
+
49
+ Get the JSON results from production, then make fixes locally in the codebase.
50
+
51
+ Process checks in priority order: critical first, then warnings.
52
+
53
+ **App items** (`source: "app"`): The user's own code. Fully actionable:
54
+
55
+ - Unused/duplicate indexes → find the index in the model's constraints, remove it, run `uv run plain makemigrations`
56
+ - Missing FK indexes → add an Index to the model, run `uv run plain makemigrations`
57
+
58
+ **Package items** (`source: "package"`): Owned by a framework or third-party package. Present these to the user with context:
59
+
60
+ - **Duplicate indexes** on package tables are likely framework bugs — mention them so the user is aware, and suggest reporting upstream
61
+ - **Unused indexes** on package tables may support features the app hasn't activated yet — these are not necessarily problems. Note them but don't recommend removal
62
+
63
+ **Unmanaged items** (`source: ""`): Tables not owned by any Plain model. Use direct SQL:
64
+
65
+ - Run the `suggestion` SQL directly (the command provides exact DDL)
66
+
67
+ **Operational issues** (sequence exhaustion, XID wraparound, vacuum health):
68
+
69
+ - These require direct database operations regardless of source
70
+ - For sequence exhaustion, alter the column type and create a migration
71
+ - For XID wraparound, investigate autovacuum configuration
72
+ - For vacuum health, check if autovacuum is keeping up
73
+
74
+ ## 4. Re-run to verify
75
+
76
+ After deploying changes, run the diagnose command again on production to confirm the issues are resolved.
@@ -14,6 +14,7 @@ from ..backups.cli import cli as backups_cli
14
14
  from ..db import get_connection
15
15
  from ..dialect import quote_name
16
16
  from ..migrations.recorder import MIGRATION_TABLE_NAME
17
+ from .diagnose import diagnose
17
18
 
18
19
 
19
20
  @register_cli("db")
@@ -23,6 +24,7 @@ def cli() -> None:
23
24
 
24
25
 
25
26
  cli.add_command(backups_cli)
27
+ cli.add_command(diagnose)
26
28
 
27
29
 
28
30
  @cli.command()