plain.postgres 0.91.0__tar.gz → 0.92.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.
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/PKG-INFO +122 -31
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/CHANGELOG.md +38 -1
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/README.md +121 -30
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/converge.py +9 -9
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/schema.py +17 -7
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/sync.py +28 -23
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/connection.py +2 -4
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/constraints.py +11 -1
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/convergence/__init__.py +10 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/convergence/analysis.py +568 -110
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/convergence/fixes.py +138 -3
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/convergence/planning.py +37 -1
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/dialect.py +0 -5
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/indexes.py +2 -53
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/introspection/__init__.py +10 -2
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/introspection/schema.py +111 -34
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/autodetector.py +1 -37
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/executor.py +2 -7
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/questioner.py +0 -26
- plain_postgres-0.92.0/plain/postgres/schema.py +570 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/utils.py +25 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/pyproject.toml +1 -1
- plain_postgres-0.92.0/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +51 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/models.py +24 -0
- plain_postgres-0.92.0/tests/conftest_convergence.py +117 -0
- plain_postgres-0.92.0/tests/test_convergence.py +857 -0
- plain_postgres-0.92.0/tests/test_convergence_constraints.py +984 -0
- plain_postgres-0.92.0/tests/test_convergence_fk.py +365 -0
- plain_postgres-0.92.0/tests/test_convergence_indexes.py +497 -0
- plain_postgres-0.92.0/tests/test_convergence_nullability.py +299 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_introspection.py +86 -28
- plain_postgres-0.92.0/tests/test_migration_executor.py +93 -0
- plain_postgres-0.91.0/plain/postgres/schema.py +0 -1652
- plain_postgres-0.91.0/tests/test_convergence.py +0 -1652
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/.gitignore +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/CLAUDE.md +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/LICENSE +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/README.md +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/connections.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/introspection/health.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/test/utils.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_iterator.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_models.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_read_only_transactions.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_related_descriptors.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_related_manager_api.py +0 -0
- {plain_postgres-0.91.0 → plain_postgres-0.92.0}/tests/test_schema_normalize_type.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.92.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
|
|
@@ -17,7 +17,10 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
- [Overview](#overview)
|
|
18
18
|
- [Database connection](#database-connection)
|
|
19
19
|
- [Querying](#querying)
|
|
20
|
-
- [
|
|
20
|
+
- [Schema management](#schema-management)
|
|
21
|
+
- [Syncing](#syncing)
|
|
22
|
+
- [Migrations](#migrations)
|
|
23
|
+
- [Convergence](#convergence)
|
|
21
24
|
- [Fields](#fields)
|
|
22
25
|
- [Relationships](#relationships)
|
|
23
26
|
- [Constraints](#constraints)
|
|
@@ -440,49 +443,86 @@ conn.set_read_only(False) # back to normal
|
|
|
440
443
|
|
|
441
444
|
Read-only mode must be set outside a transaction — calling it inside `atomic()` raises `TransactionManagementError`.
|
|
442
445
|
|
|
443
|
-
##
|
|
446
|
+
## Schema management
|
|
444
447
|
|
|
445
|
-
|
|
448
|
+
Your database schema is managed by two complementary systems: **migrations** and **convergence**. Migrations handle structural changes — creating tables, adding columns, renaming things. Convergence handles everything declarative — indexes, constraints, foreign keys, and NOT NULL enforcement. You declare these on your models and convergence makes the database match.
|
|
446
449
|
|
|
447
|
-
|
|
450
|
+
```
|
|
451
|
+
Migrations Convergence
|
|
452
|
+
(imperative, ordered) (declarative, idempotent)
|
|
453
|
+
───────────────────────────── ─────────────────────────────
|
|
454
|
+
Create / drop tables Indexes
|
|
455
|
+
Add / remove / rename columns Check constraints
|
|
456
|
+
Change column types Unique constraints
|
|
457
|
+
Data migrations (RunPython) Foreign key constraints
|
|
458
|
+
Custom SQL (RunSQL) NOT NULL enforcement
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
This split exists because structural changes (adding a column) must happen in a specific order and can't be retried, while declarative objects (indexes, constraints) can be compared against model definitions and fixed automatically — even if a previous attempt failed.
|
|
462
|
+
|
|
463
|
+
### Syncing
|
|
464
|
+
|
|
465
|
+
The primary command for all schema management is `postgres sync`. It runs migrations and convergence together:
|
|
448
466
|
|
|
449
467
|
```bash
|
|
450
|
-
plain
|
|
468
|
+
plain postgres sync
|
|
451
469
|
```
|
|
452
470
|
|
|
453
|
-
|
|
471
|
+
```
|
|
472
|
+
plain postgres sync
|
|
473
|
+
│
|
|
474
|
+
├─ 1. Create migrations (development only)
|
|
475
|
+
│ Detects model changes, generates migration files
|
|
476
|
+
│
|
|
477
|
+
├─ 2. Apply migrations
|
|
478
|
+
│ Runs pending migrations in a single transaction
|
|
479
|
+
│
|
|
480
|
+
└─ 3. Converge
|
|
481
|
+
Compares indexes, constraints, FKs, and nullability
|
|
482
|
+
against model declarations — applies fixes independently
|
|
483
|
+
```
|
|
454
484
|
|
|
455
|
-
|
|
456
|
-
- `--check` — Exit non-zero if migrations are needed (for CI)
|
|
457
|
-
- `--empty <package>` — Create an empty migration for custom data migrations
|
|
458
|
-
- `--name <name>` — Set the migration filename
|
|
459
|
-
- `-v 3` — Show full migration file contents
|
|
485
|
+
In development (`DEBUG=True`), sync auto-generates migrations before applying them. In production, it only applies existing migrations and converges.
|
|
460
486
|
|
|
461
|
-
|
|
487
|
+
| Command | Purpose |
|
|
488
|
+
| --------------------------------------- | --------------------------------------------------------------------- |
|
|
489
|
+
| `plain postgres sync` | Create + apply migrations + converge (the one command for everything) |
|
|
490
|
+
| `plain postgres sync --check` | Exit non-zero if anything would change (for CI) |
|
|
491
|
+
| `plain postgres sync --drop-undeclared` | Also remove indexes/constraints not declared on any model |
|
|
492
|
+
| `plain postgres schema` | Show schema state with drift detection |
|
|
493
|
+
| `plain postgres schema --json` | Machine-readable schema output |
|
|
494
|
+
| `plain postgres converge` | Run convergence alone (advanced) |
|
|
462
495
|
|
|
463
|
-
###
|
|
496
|
+
### Migrations
|
|
497
|
+
|
|
498
|
+
Migrations track structural changes to your models. They are Python files stored in your app's `migrations/` directory.
|
|
464
499
|
|
|
465
500
|
```bash
|
|
466
|
-
plain migrations
|
|
501
|
+
plain migrations create
|
|
467
502
|
```
|
|
468
503
|
|
|
469
504
|
Key flags:
|
|
470
505
|
|
|
471
|
-
- `--
|
|
472
|
-
- `--check` — Exit non-zero if
|
|
473
|
-
- `--
|
|
474
|
-
|
|
475
|
-
### Viewing migration status
|
|
506
|
+
- `--dry-run` — Show what migrations would be created (with operations and SQL) without writing files
|
|
507
|
+
- `--check` — Exit non-zero if migrations are needed (for CI)
|
|
508
|
+
- `--empty <package>` — Create an empty migration for custom data migrations
|
|
509
|
+
- `--name <name>` — Set the migration filename
|
|
476
510
|
|
|
477
|
-
|
|
478
|
-
plain migrations list
|
|
479
|
-
```
|
|
511
|
+
Only write migrations by hand if they are custom data migrations.
|
|
480
512
|
|
|
481
|
-
|
|
513
|
+
Other migration commands:
|
|
482
514
|
|
|
483
|
-
|
|
515
|
+
| Command | Purpose |
|
|
516
|
+
| ------------------------------------------- | ---------------------------------------------------- |
|
|
517
|
+
| `plain migrations apply` | Apply pending migrations |
|
|
518
|
+
| `plain migrations apply --plan` | Preview what would run |
|
|
519
|
+
| `plain migrations apply --check` | Exit non-zero if unapplied migrations exist (for CI) |
|
|
520
|
+
| `plain migrations apply --fake` | Mark as applied without running SQL |
|
|
521
|
+
| `plain migrations list` | View migration status by package |
|
|
522
|
+
| `plain migrations squash <pkg> <migration>` | Squash migrations into one |
|
|
523
|
+
| `plain migrations prune` | Remove stale migration records |
|
|
484
524
|
|
|
485
|
-
|
|
525
|
+
#### Development workflow
|
|
486
526
|
|
|
487
527
|
During development, iterating on models often produces multiple small migrations (0002, 0003, 0004...). Clean these up before committing.
|
|
488
528
|
|
|
@@ -509,7 +549,7 @@ Use this when migrations have already been committed or deployed to other enviro
|
|
|
509
549
|
| Migrations are committed but not deployed | Delete-and-recreate (if all developers reset) or squash |
|
|
510
550
|
| Migrations are deployed to production | Squash or full reset |
|
|
511
551
|
|
|
512
|
-
|
|
552
|
+
#### Resetting migrations
|
|
513
553
|
|
|
514
554
|
Over time a package can accumulate dozens of migrations. Once **every environment** (dev, staging, production) has applied all of them, you can replace the entire history with a single fresh `0001_initial`.
|
|
515
555
|
|
|
@@ -533,10 +573,61 @@ Over time a package can accumulate dozens of migrations. Once **every environmen
|
|
|
533
573
|
- Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
|
|
534
574
|
- If CI runs `migrations create --check` or `migrations apply --check`, the reset PR must be merged and deployed before those checks pass in other branches.
|
|
535
575
|
|
|
536
|
-
###
|
|
576
|
+
### Convergence
|
|
577
|
+
|
|
578
|
+
Convergence compares the indexes, constraints, foreign keys, and nullability declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
|
|
579
|
+
|
|
580
|
+
```python
|
|
581
|
+
@postgres.register_model
|
|
582
|
+
class User(postgres.Model):
|
|
583
|
+
email: str = types.EmailField()
|
|
584
|
+
username: str = types.TextField(max_length=150)
|
|
585
|
+
age: int = types.IntegerField()
|
|
586
|
+
|
|
587
|
+
model_options = postgres.Options(
|
|
588
|
+
indexes=[
|
|
589
|
+
postgres.Index(fields=["email"], name="users_email_idx"),
|
|
590
|
+
],
|
|
591
|
+
constraints=[
|
|
592
|
+
postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
|
|
593
|
+
postgres.CheckConstraint(check=postgres.Q(age__gte=0), name="users_age_positive"),
|
|
594
|
+
],
|
|
595
|
+
)
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
When you run `postgres sync`, convergence detects that these indexes and constraints are missing and creates them — using non-blocking DDL operations where possible (e.g. `CREATE INDEX CONCURRENTLY`, `ADD CONSTRAINT ... NOT VALID` followed by `VALIDATE CONSTRAINT`).
|
|
599
|
+
|
|
600
|
+
**Key properties:**
|
|
601
|
+
|
|
602
|
+
- **Idempotent** — safe to run repeatedly. If the database already matches, nothing happens.
|
|
603
|
+
- **Non-blocking** — indexes are built with `CONCURRENTLY`, constraints use `NOT VALID` + `VALIDATE` to avoid locking writes.
|
|
604
|
+
- **Per-operation commits** — each fix is committed independently so a single failure doesn't roll back other fixes.
|
|
605
|
+
- **Self-healing** — detects and rebuilds `INVALID` indexes (e.g. from a previously failed `CREATE INDEX CONCURRENTLY`).
|
|
606
|
+
- **Rename-aware** — detects renamed indexes and constraints by matching their structure, avoiding unnecessary drop + recreate.
|
|
607
|
+
|
|
608
|
+
**Inspecting schema state:**
|
|
609
|
+
|
|
610
|
+
Use `postgres schema` to see what convergence would do. It shows every model's columns, indexes, and constraints compared against the database, with issues highlighted:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
plain postgres schema # all models
|
|
614
|
+
plain postgres schema User # single model
|
|
615
|
+
plain postgres schema --json # machine-readable output
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
**Staged rollouts:**
|
|
619
|
+
|
|
620
|
+
Some changes can't be applied automatically. For example, if you add `NOT NULL` to a column that has existing `NULL` rows, convergence will report this as a blocked change and tell you to backfill the data first. Run `postgres sync` again after the backfill.
|
|
621
|
+
|
|
622
|
+
**Cleanup:**
|
|
623
|
+
|
|
624
|
+
When you remove an index or constraint from a model, the database object still exists. By default, `postgres sync` reports undeclared objects but doesn't drop them. Use `--drop-undeclared` to remove them:
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
plain postgres sync --drop-undeclared
|
|
628
|
+
```
|
|
537
629
|
|
|
538
|
-
|
|
539
|
-
- `plain migrations prune` — Remove stale migration records
|
|
630
|
+
Undeclared constraints block sync (they affect query behavior), while undeclared indexes are just reported as warnings.
|
|
540
631
|
|
|
541
632
|
## Fields
|
|
542
633
|
|
|
@@ -795,7 +886,7 @@ Field-level validation happens automatically based on field types and constraint
|
|
|
795
886
|
|
|
796
887
|
### Indexes and constraints
|
|
797
888
|
|
|
798
|
-
You can optimize queries and ensure data integrity with indexes and constraints
|
|
889
|
+
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
799
890
|
|
|
800
891
|
```python
|
|
801
892
|
class User(postgres.Model):
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.92.0](https://github.com/dropseed/plain/releases/plain-postgres@0.92.0) (2026-03-30)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **Foreign key constraints are now managed by convergence, not migrations.** The schema editor no longer creates, drops, or alters FK constraints — convergence handles them declaratively using `ADD CONSTRAINT ... NOT VALID` followed by `VALIDATE CONSTRAINT`. FK constraint names are deterministic and match the old migration-generated names. ([b2b968297fea](https://github.com/dropseed/plain/commit/b2b968297fea), [8658be035a46](https://github.com/dropseed/plain/commit/8658be035a46))
|
|
8
|
+
|
|
9
|
+
- **NOT NULL enforcement is now managed by convergence.** Column nullability drift is detected and fixed automatically — convergence uses the safe `CHECK NOT VALID → VALIDATE → SET NOT NULL` pattern to avoid long table locks. Columns with existing NULL rows are reported as blocked, requiring a backfill before convergence can proceed. ([5ea3dc589453](https://github.com/dropseed/plain/commit/5ea3dc589453))
|
|
10
|
+
|
|
11
|
+
- **Managed type boundaries** — convergence now distinguishes managed vs unmanaged index types and constraint types. Only btree/hash indexes and check/unique/FK constraints participate in drift detection and rename matching. Unmanaged types (GIN, GiST, BRIN, exclusion, trigger) are displayed for informational purposes but are never modified or reported as undeclared. ([f123eae2fa56](https://github.com/dropseed/plain/commit/f123eae2fa56))
|
|
12
|
+
|
|
13
|
+
- **Unique constraint drift detection** — convergence now compares unique constraint definitions (not just column lists), detecting behavioral changes like modified WHERE clauses, opclasses, or expressions. Index-only uniques (partial, expression, or opclass) are correctly handled through both pg_constraint and pg_index. ([09b439e8448a](https://github.com/dropseed/plain/commit/09b439e8448a))
|
|
14
|
+
|
|
15
|
+
- **Full index definition matching** — index drift detection now compares normalized `CREATE INDEX` definitions instead of just column lists, catching changes to conditions, expressions, opclasses, and include columns. ([70d7a6725498](https://github.com/dropseed/plain/commit/70d7a6725498))
|
|
16
|
+
|
|
17
|
+
- Removed dead index/constraint/deferred SQL infrastructure and primary-key transition code from the schema editor. ([266b0635f0bf](https://github.com/dropseed/plain/commit/266b0635f0bf), [4a92f5479e4e](https://github.com/dropseed/plain/commit/4a92f5479e4e))
|
|
18
|
+
|
|
19
|
+
- Rewrote the introspection layer to mirror Postgres catalog structures — `TableState` now uses a unified `constraints` dict keyed by constraint name with `ConType` enum, replacing the separate `unique_constraints`, `check_constraints`, and `foreign_keys` dicts. ([f123eae2fa56](https://github.com/dropseed/plain/commit/f123eae2fa56))
|
|
20
|
+
|
|
21
|
+
- Expanded schema management documentation with a comprehensive overview of the migrations + convergence split, sync workflow, and convergence behavior. ([57caeee5ff89](https://github.com/dropseed/plain/commit/57caeee5ff89))
|
|
22
|
+
|
|
23
|
+
### Upgrade instructions
|
|
24
|
+
|
|
25
|
+
- If you have custom code that interacts with `TableState.unique_constraints`, `TableState.check_constraints`, or `TableState.foreign_keys`, update it to use the unified `TableState.constraints` dict with `ConType` filtering instead.
|
|
26
|
+
- FK constraints in existing databases are left as-is. New FKs will be created by convergence on the next `postgres sync`.
|
|
27
|
+
- NOT NULL enforcement is automatic — `postgres sync` will detect and fix nullability drift. If columns have existing NULL rows, you'll need to backfill before convergence can apply NOT NULL.
|
|
28
|
+
|
|
29
|
+
## [0.91.1](https://github.com/dropseed/plain/releases/plain-postgres@0.91.1) (2026-03-29)
|
|
30
|
+
|
|
31
|
+
### What's changed
|
|
32
|
+
|
|
33
|
+
- Indented `sync` and `converge` sub-items under section headers for readability in environments without ANSI colors (e.g. Heroku deploy logs). ([b6b494dcc698](https://github.com/dropseed/plain/commit/b6b494dcc698))
|
|
34
|
+
- `sync` now uses `MigrationExecutor` directly instead of calling through the CLI layer, giving cleaner indented output. ([b6b494dcc698](https://github.com/dropseed/plain/commit/b6b494dcc698))
|
|
35
|
+
|
|
36
|
+
### Upgrade instructions
|
|
37
|
+
|
|
38
|
+
- No changes required.
|
|
39
|
+
|
|
3
40
|
## [0.91.0](https://github.com/dropseed/plain/releases/plain-postgres@0.91.0) (2026-03-29)
|
|
4
41
|
|
|
5
42
|
### What's changed
|
|
@@ -22,7 +59,7 @@
|
|
|
22
59
|
|
|
23
60
|
1. **Replace `migrate` with `postgres sync` in deploy scripts and CI.** `postgres sync` applies migrations and runs convergence in a single step. For CI checks, use `postgres sync --check` instead of `migrate --check` / `makemigrations --check`. The lower-level commands are still available as `migrations create` and `migrations apply`.
|
|
24
61
|
|
|
25
|
-
2. **Remove index/constraint operations from migration files.** Delete any `AddIndex`, `RemoveIndex`, `RenameIndex`, `AddConstraint`, and `RemoveConstraint` operations from your migration files — these classes no longer exist and will cause import errors.
|
|
62
|
+
2. **Remove index/constraint operations from migration files.** Delete any `AddIndex`, `RemoveIndex`, `RenameIndex`, `AddConstraint`, and `RemoveConstraint` operations from your migration files — these classes no longer exist and will cause import errors. It's fine to leave a migration with `operations = []`. Indexes and constraints declared on your models will be created automatically by convergence.
|
|
26
63
|
|
|
27
64
|
3. **Replace `PositiveIntegerField`** (and `PositiveBigIntegerField`, `PositiveSmallIntegerField`) with `IntegerField` (or `BigIntegerField`, `SmallIntegerField`) in both models and migration files. Add a `CheckConstraint` if you need to enforce positive values.
|
|
28
65
|
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
6
|
- [Database connection](#database-connection)
|
|
7
7
|
- [Querying](#querying)
|
|
8
|
-
- [
|
|
8
|
+
- [Schema management](#schema-management)
|
|
9
|
+
- [Syncing](#syncing)
|
|
10
|
+
- [Migrations](#migrations)
|
|
11
|
+
- [Convergence](#convergence)
|
|
9
12
|
- [Fields](#fields)
|
|
10
13
|
- [Relationships](#relationships)
|
|
11
14
|
- [Constraints](#constraints)
|
|
@@ -428,49 +431,86 @@ conn.set_read_only(False) # back to normal
|
|
|
428
431
|
|
|
429
432
|
Read-only mode must be set outside a transaction — calling it inside `atomic()` raises `TransactionManagementError`.
|
|
430
433
|
|
|
431
|
-
##
|
|
434
|
+
## Schema management
|
|
432
435
|
|
|
433
|
-
|
|
436
|
+
Your database schema is managed by two complementary systems: **migrations** and **convergence**. Migrations handle structural changes — creating tables, adding columns, renaming things. Convergence handles everything declarative — indexes, constraints, foreign keys, and NOT NULL enforcement. You declare these on your models and convergence makes the database match.
|
|
434
437
|
|
|
435
|
-
|
|
438
|
+
```
|
|
439
|
+
Migrations Convergence
|
|
440
|
+
(imperative, ordered) (declarative, idempotent)
|
|
441
|
+
───────────────────────────── ─────────────────────────────
|
|
442
|
+
Create / drop tables Indexes
|
|
443
|
+
Add / remove / rename columns Check constraints
|
|
444
|
+
Change column types Unique constraints
|
|
445
|
+
Data migrations (RunPython) Foreign key constraints
|
|
446
|
+
Custom SQL (RunSQL) NOT NULL enforcement
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
This split exists because structural changes (adding a column) must happen in a specific order and can't be retried, while declarative objects (indexes, constraints) can be compared against model definitions and fixed automatically — even if a previous attempt failed.
|
|
450
|
+
|
|
451
|
+
### Syncing
|
|
452
|
+
|
|
453
|
+
The primary command for all schema management is `postgres sync`. It runs migrations and convergence together:
|
|
436
454
|
|
|
437
455
|
```bash
|
|
438
|
-
plain
|
|
456
|
+
plain postgres sync
|
|
439
457
|
```
|
|
440
458
|
|
|
441
|
-
|
|
459
|
+
```
|
|
460
|
+
plain postgres sync
|
|
461
|
+
│
|
|
462
|
+
├─ 1. Create migrations (development only)
|
|
463
|
+
│ Detects model changes, generates migration files
|
|
464
|
+
│
|
|
465
|
+
├─ 2. Apply migrations
|
|
466
|
+
│ Runs pending migrations in a single transaction
|
|
467
|
+
│
|
|
468
|
+
└─ 3. Converge
|
|
469
|
+
Compares indexes, constraints, FKs, and nullability
|
|
470
|
+
against model declarations — applies fixes independently
|
|
471
|
+
```
|
|
442
472
|
|
|
443
|
-
|
|
444
|
-
- `--check` — Exit non-zero if migrations are needed (for CI)
|
|
445
|
-
- `--empty <package>` — Create an empty migration for custom data migrations
|
|
446
|
-
- `--name <name>` — Set the migration filename
|
|
447
|
-
- `-v 3` — Show full migration file contents
|
|
473
|
+
In development (`DEBUG=True`), sync auto-generates migrations before applying them. In production, it only applies existing migrations and converges.
|
|
448
474
|
|
|
449
|
-
|
|
475
|
+
| Command | Purpose |
|
|
476
|
+
| --------------------------------------- | --------------------------------------------------------------------- |
|
|
477
|
+
| `plain postgres sync` | Create + apply migrations + converge (the one command for everything) |
|
|
478
|
+
| `plain postgres sync --check` | Exit non-zero if anything would change (for CI) |
|
|
479
|
+
| `plain postgres sync --drop-undeclared` | Also remove indexes/constraints not declared on any model |
|
|
480
|
+
| `plain postgres schema` | Show schema state with drift detection |
|
|
481
|
+
| `plain postgres schema --json` | Machine-readable schema output |
|
|
482
|
+
| `plain postgres converge` | Run convergence alone (advanced) |
|
|
450
483
|
|
|
451
|
-
###
|
|
484
|
+
### Migrations
|
|
485
|
+
|
|
486
|
+
Migrations track structural changes to your models. They are Python files stored in your app's `migrations/` directory.
|
|
452
487
|
|
|
453
488
|
```bash
|
|
454
|
-
plain migrations
|
|
489
|
+
plain migrations create
|
|
455
490
|
```
|
|
456
491
|
|
|
457
492
|
Key flags:
|
|
458
493
|
|
|
459
|
-
- `--
|
|
460
|
-
- `--check` — Exit non-zero if
|
|
461
|
-
- `--
|
|
462
|
-
|
|
463
|
-
### Viewing migration status
|
|
494
|
+
- `--dry-run` — Show what migrations would be created (with operations and SQL) without writing files
|
|
495
|
+
- `--check` — Exit non-zero if migrations are needed (for CI)
|
|
496
|
+
- `--empty <package>` — Create an empty migration for custom data migrations
|
|
497
|
+
- `--name <name>` — Set the migration filename
|
|
464
498
|
|
|
465
|
-
|
|
466
|
-
plain migrations list
|
|
467
|
-
```
|
|
499
|
+
Only write migrations by hand if they are custom data migrations.
|
|
468
500
|
|
|
469
|
-
|
|
501
|
+
Other migration commands:
|
|
470
502
|
|
|
471
|
-
|
|
503
|
+
| Command | Purpose |
|
|
504
|
+
| ------------------------------------------- | ---------------------------------------------------- |
|
|
505
|
+
| `plain migrations apply` | Apply pending migrations |
|
|
506
|
+
| `plain migrations apply --plan` | Preview what would run |
|
|
507
|
+
| `plain migrations apply --check` | Exit non-zero if unapplied migrations exist (for CI) |
|
|
508
|
+
| `plain migrations apply --fake` | Mark as applied without running SQL |
|
|
509
|
+
| `plain migrations list` | View migration status by package |
|
|
510
|
+
| `plain migrations squash <pkg> <migration>` | Squash migrations into one |
|
|
511
|
+
| `plain migrations prune` | Remove stale migration records |
|
|
472
512
|
|
|
473
|
-
|
|
513
|
+
#### Development workflow
|
|
474
514
|
|
|
475
515
|
During development, iterating on models often produces multiple small migrations (0002, 0003, 0004...). Clean these up before committing.
|
|
476
516
|
|
|
@@ -497,7 +537,7 @@ Use this when migrations have already been committed or deployed to other enviro
|
|
|
497
537
|
| Migrations are committed but not deployed | Delete-and-recreate (if all developers reset) or squash |
|
|
498
538
|
| Migrations are deployed to production | Squash or full reset |
|
|
499
539
|
|
|
500
|
-
|
|
540
|
+
#### Resetting migrations
|
|
501
541
|
|
|
502
542
|
Over time a package can accumulate dozens of migrations. Once **every environment** (dev, staging, production) has applied all of them, you can replace the entire history with a single fresh `0001_initial`.
|
|
503
543
|
|
|
@@ -521,10 +561,61 @@ Over time a package can accumulate dozens of migrations. Once **every environmen
|
|
|
521
561
|
- Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
|
|
522
562
|
- If CI runs `migrations create --check` or `migrations apply --check`, the reset PR must be merged and deployed before those checks pass in other branches.
|
|
523
563
|
|
|
524
|
-
###
|
|
564
|
+
### Convergence
|
|
565
|
+
|
|
566
|
+
Convergence compares the indexes, constraints, foreign keys, and nullability declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
@postgres.register_model
|
|
570
|
+
class User(postgres.Model):
|
|
571
|
+
email: str = types.EmailField()
|
|
572
|
+
username: str = types.TextField(max_length=150)
|
|
573
|
+
age: int = types.IntegerField()
|
|
574
|
+
|
|
575
|
+
model_options = postgres.Options(
|
|
576
|
+
indexes=[
|
|
577
|
+
postgres.Index(fields=["email"], name="users_email_idx"),
|
|
578
|
+
],
|
|
579
|
+
constraints=[
|
|
580
|
+
postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
|
|
581
|
+
postgres.CheckConstraint(check=postgres.Q(age__gte=0), name="users_age_positive"),
|
|
582
|
+
],
|
|
583
|
+
)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
When you run `postgres sync`, convergence detects that these indexes and constraints are missing and creates them — using non-blocking DDL operations where possible (e.g. `CREATE INDEX CONCURRENTLY`, `ADD CONSTRAINT ... NOT VALID` followed by `VALIDATE CONSTRAINT`).
|
|
587
|
+
|
|
588
|
+
**Key properties:**
|
|
589
|
+
|
|
590
|
+
- **Idempotent** — safe to run repeatedly. If the database already matches, nothing happens.
|
|
591
|
+
- **Non-blocking** — indexes are built with `CONCURRENTLY`, constraints use `NOT VALID` + `VALIDATE` to avoid locking writes.
|
|
592
|
+
- **Per-operation commits** — each fix is committed independently so a single failure doesn't roll back other fixes.
|
|
593
|
+
- **Self-healing** — detects and rebuilds `INVALID` indexes (e.g. from a previously failed `CREATE INDEX CONCURRENTLY`).
|
|
594
|
+
- **Rename-aware** — detects renamed indexes and constraints by matching their structure, avoiding unnecessary drop + recreate.
|
|
595
|
+
|
|
596
|
+
**Inspecting schema state:**
|
|
597
|
+
|
|
598
|
+
Use `postgres schema` to see what convergence would do. It shows every model's columns, indexes, and constraints compared against the database, with issues highlighted:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
plain postgres schema # all models
|
|
602
|
+
plain postgres schema User # single model
|
|
603
|
+
plain postgres schema --json # machine-readable output
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Staged rollouts:**
|
|
607
|
+
|
|
608
|
+
Some changes can't be applied automatically. For example, if you add `NOT NULL` to a column that has existing `NULL` rows, convergence will report this as a blocked change and tell you to backfill the data first. Run `postgres sync` again after the backfill.
|
|
609
|
+
|
|
610
|
+
**Cleanup:**
|
|
611
|
+
|
|
612
|
+
When you remove an index or constraint from a model, the database object still exists. By default, `postgres sync` reports undeclared objects but doesn't drop them. Use `--drop-undeclared` to remove them:
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
plain postgres sync --drop-undeclared
|
|
616
|
+
```
|
|
525
617
|
|
|
526
|
-
|
|
527
|
-
- `plain migrations prune` — Remove stale migration records
|
|
618
|
+
Undeclared constraints block sync (they affect query behavior), while undeclared indexes are just reported as warnings.
|
|
528
619
|
|
|
529
620
|
## Fields
|
|
530
621
|
|
|
@@ -783,7 +874,7 @@ Field-level validation happens automatically based on field types and constraint
|
|
|
783
874
|
|
|
784
875
|
### Indexes and constraints
|
|
785
876
|
|
|
786
|
-
You can optimize queries and ensure data integrity with indexes and constraints
|
|
877
|
+
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
787
878
|
|
|
788
879
|
```python
|
|
789
880
|
class User(postgres.Model):
|
|
@@ -66,32 +66,32 @@ def converge(yes: bool, drop_undeclared: bool) -> None:
|
|
|
66
66
|
click.secho(f" FAILED: {r.item.describe()} — {r.error}", fg="red")
|
|
67
67
|
|
|
68
68
|
click.echo()
|
|
69
|
-
click.secho(result.summary, fg="green" if result.ok else "yellow")
|
|
69
|
+
click.secho(f" {result.summary}", fg="green" if result.ok else "yellow")
|
|
70
70
|
if not result.ok_for_sync:
|
|
71
71
|
success = False
|
|
72
72
|
|
|
73
73
|
if plan.blocked:
|
|
74
74
|
click.echo()
|
|
75
|
-
click.secho("Schema changes require a staged rollout:", fg="red", bold=True)
|
|
75
|
+
click.secho(" Schema changes require a staged rollout:", fg="red", bold=True)
|
|
76
76
|
for item in plan.blocked:
|
|
77
|
-
click.secho(f"
|
|
77
|
+
click.secho(f" {item.drift.describe()}", fg="red")
|
|
78
78
|
if item.guidance:
|
|
79
|
-
click.secho(f"
|
|
79
|
+
click.secho(f" {item.guidance}", fg="red", dim=True)
|
|
80
80
|
success = False
|
|
81
81
|
|
|
82
82
|
if not drop_undeclared and plan.blocking_cleanup:
|
|
83
83
|
click.echo()
|
|
84
|
-
click.secho("Undeclared constraints still in database:", fg="red", bold=True)
|
|
84
|
+
click.secho(" Undeclared constraints still in database:", fg="red", bold=True)
|
|
85
85
|
for item in plan.blocking_cleanup:
|
|
86
|
-
click.secho(f"
|
|
87
|
-
click.secho("Rerun with --drop-undeclared to remove them.", fg="red")
|
|
86
|
+
click.secho(f" {item.describe()}", fg="red")
|
|
87
|
+
click.secho(" Rerun with --drop-undeclared to remove them.", fg="red")
|
|
88
88
|
success = False
|
|
89
89
|
|
|
90
90
|
if not drop_undeclared and plan.optional_cleanup:
|
|
91
91
|
click.echo()
|
|
92
92
|
for item in plan.optional_cleanup:
|
|
93
|
-
click.echo(f"
|
|
94
|
-
click.echo("Run with --drop-undeclared to remove undeclared indexes.")
|
|
93
|
+
click.echo(f" {item.describe()}")
|
|
94
|
+
click.echo(" Run with --drop-undeclared to remove undeclared indexes.")
|
|
95
95
|
|
|
96
96
|
if not success:
|
|
97
97
|
sys.exit(1)
|
|
@@ -8,7 +8,7 @@ import click
|
|
|
8
8
|
from ..convergence.analysis import ModelAnalysis, analyze_model
|
|
9
9
|
from ..convergence.planning import can_auto_fix
|
|
10
10
|
from ..db import get_connection
|
|
11
|
-
from ..introspection import get_unknown_tables
|
|
11
|
+
from ..introspection import MANAGED_CONSTRAINT_TYPES, get_unknown_tables
|
|
12
12
|
from ..registry import models_registry
|
|
13
13
|
|
|
14
14
|
|
|
@@ -24,6 +24,10 @@ def _fixable(msg: str) -> None:
|
|
|
24
24
|
click.secho(f" ~ {msg} (auto-fix)", fg="yellow")
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _unmanaged(type_label: str) -> None:
|
|
28
|
+
click.secho(f" unmanaged ({type_label})", dim=True)
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
def _render_model(analysis: ModelAnalysis) -> None:
|
|
28
32
|
"""Render a model analysis result as human-readable output."""
|
|
29
33
|
click.secho(analysis.label, bold=True, nl=False)
|
|
@@ -52,7 +56,9 @@ def _render_model(analysis: ModelAnalysis) -> None:
|
|
|
52
56
|
|
|
53
57
|
click.echo(f" {col_display:30s} {' '.join(type_parts)}", nl=False)
|
|
54
58
|
|
|
55
|
-
if col.issue:
|
|
59
|
+
if col.issue and col.drift and can_auto_fix(col.drift):
|
|
60
|
+
_fixable(col.issue)
|
|
61
|
+
elif col.issue:
|
|
56
62
|
_err(col.issue)
|
|
57
63
|
else:
|
|
58
64
|
_ok()
|
|
@@ -66,7 +72,9 @@ def _render_model(analysis: ModelAnalysis) -> None:
|
|
|
66
72
|
fields_str = ", ".join(idx.fields) if idx.fields else "expressions"
|
|
67
73
|
click.echo(f" {idx.name} ({fields_str})", nl=False)
|
|
68
74
|
|
|
69
|
-
if idx.
|
|
75
|
+
if idx.access_method:
|
|
76
|
+
_unmanaged(idx.access_method)
|
|
77
|
+
elif idx.issue and idx.drift and can_auto_fix(idx.drift):
|
|
70
78
|
_fixable(idx.issue)
|
|
71
79
|
elif idx.issue:
|
|
72
80
|
_err(idx.issue)
|
|
@@ -79,15 +87,17 @@ def _render_model(analysis: ModelAnalysis) -> None:
|
|
|
79
87
|
click.secho(" Constraints:", dim=True)
|
|
80
88
|
|
|
81
89
|
for con in analysis.constraints:
|
|
82
|
-
|
|
90
|
+
con_label = con.constraint_type.label.upper()
|
|
83
91
|
if con.fields:
|
|
84
92
|
click.echo(
|
|
85
|
-
f" {con.name} {
|
|
93
|
+
f" {con.name} {con_label} ({', '.join(con.fields)})", nl=False
|
|
86
94
|
)
|
|
87
95
|
else:
|
|
88
|
-
click.echo(f" {con.name} {
|
|
96
|
+
click.echo(f" {con.name} {con_label}", nl=False)
|
|
89
97
|
|
|
90
|
-
if con.
|
|
98
|
+
if con.constraint_type not in MANAGED_CONSTRAINT_TYPES:
|
|
99
|
+
_unmanaged(con.constraint_type.label)
|
|
100
|
+
elif con.issue and con.drift and can_auto_fix(con.drift):
|
|
91
101
|
_fixable(con.issue)
|
|
92
102
|
elif con.issue:
|
|
93
103
|
_err(con.issue)
|