plain.postgres 0.96.0__tar.gz → 0.97.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.96.0 → plain_postgres-0.97.0}/PKG-INFO +61 -33
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/CHANGELOG.md +30 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/README.md +60 -32
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/__init__.py +2 -1
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/converge.py +2 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/core.py +3 -0
- plain_postgres-0.97.0/plain/postgres/cli/decorators.py +24 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/diagnose.py +2 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/migrations.py +6 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/schema.py +2 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/sync.py +2 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/connection.py +37 -384
- plain_postgres-0.97.0/plain/postgres/connections.py +126 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/database_url.py +24 -2
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/db.py +2 -1
- plain_postgres-0.97.0/plain/postgres/default_settings.py +38 -0
- plain_postgres-0.97.0/plain/postgres/test/database.py +150 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/test/pytest.py +24 -24
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/utils.py +2 -21
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/pyproject.toml +1 -1
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_connection_lifecycle.py +25 -18
- plain_postgres-0.97.0/tests/test_management_connection.py +105 -0
- plain_postgres-0.96.0/plain/postgres/connections.py +0 -98
- plain_postgres-0.96.0/plain/postgres/default_settings.py +0 -56
- plain_postgres-0.96.0/plain/postgres/test/utils.py +0 -18
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/.gitignore +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/CLAUDE.md +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/LICENSE +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/README.md +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/health.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_constraints.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_defaults.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_fk.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_indexes.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_nullability.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_field_defaults.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_functions_uuid.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_introspection.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_iterator.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_m2m.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_migration_executor.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_mixins.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_random_string_field.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_raw_query.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_read_only_transactions.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_related.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_schema_timeouts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.97.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
|
|
@@ -82,26 +82,63 @@ admin_users = User.query.filter(is_admin=True)
|
|
|
82
82
|
|
|
83
83
|
## Database connection
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Configure the database with a single URL. The canonical Plain setting is `POSTGRES_URL`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# app/settings.py
|
|
89
|
+
POSTGRES_URL = "postgresql://user:password@localhost:5432/dbname"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or via environment variable:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
PLAIN_POSTGRES_URL=postgresql://user:password@localhost:5432/dbname
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Plain also reads the `DATABASE_URL` environment variable as a fallback — it's the widely-used convention for Postgres connection strings, so most hosting setups work without extra configuration:
|
|
86
99
|
|
|
87
100
|
```sh
|
|
88
101
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
89
102
|
```
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
Precedence (highest to lowest): `PLAIN_POSTGRES_URL` → `POSTGRES_URL` in `settings.py` → `DATABASE_URL` environment variable.
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
POSTGRES_PASSWORD = "password"
|
|
106
|
+
The URL supports any libpq connection parameter as a query string — for example `?sslmode=require&application_name=web&connect_timeout=10`. These are parsed and passed through to the driver.
|
|
107
|
+
|
|
108
|
+
To explicitly disable the database (e.g. during Docker builds where no database is available), set the URL to the string `none`:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
PLAIN_POSTGRES_URL=none
|
|
100
112
|
```
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
### Bypassing a connection pooler for management operations
|
|
115
|
+
|
|
116
|
+
Transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, standalone pgbouncer in transaction mode) can't run DDL, long transactions, or `pg_dump`. To work around this, set a second URL that management commands use to reach Postgres directly:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
PLAIN_POSTGRES_URL=postgresql://app@pooler:6432/myapp
|
|
120
|
+
PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
|
|
124
|
+
|
|
125
|
+
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`, `plain migrations squash`
|
|
126
|
+
- `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
|
|
127
|
+
- `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
|
|
128
|
+
|
|
129
|
+
When it's unset, all commands use `POSTGRES_URL` — there's no behavior change for existing apps.
|
|
130
|
+
|
|
131
|
+
To route custom code through the management connection, use the `use_management_connection()` context manager:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from plain.postgres import use_management_connection
|
|
135
|
+
|
|
136
|
+
with use_management_connection():
|
|
137
|
+
# Any get_connection() / ORM calls inside this block use POSTGRES_MANAGEMENT_URL.
|
|
138
|
+
run_custom_schema_change()
|
|
139
|
+
```
|
|
103
140
|
|
|
104
|
-
|
|
141
|
+
You _can_ point the two URLs at different Postgres roles — e.g. a least-privilege DML role for runtime and a DDL-capable role for management. Plain does not currently automate the grant/ownership plumbing that split requires (default privileges for newly-created tables, ownership reassignment, preflight checks that the runtime role can see the schema). If you adopt that pattern, you're responsible for wiring those up yourself.
|
|
105
142
|
|
|
106
143
|
**PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
|
|
107
144
|
|
|
@@ -1206,27 +1243,18 @@ These are static, code-level checks that catch issues before you deploy. The `di
|
|
|
1206
1243
|
|
|
1207
1244
|
## Settings
|
|
1208
1245
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
|
1216
|
-
|
|
|
1217
|
-
| `
|
|
1218
|
-
| `
|
|
1219
|
-
| `
|
|
1220
|
-
| `
|
|
1221
|
-
| `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
|
|
1222
|
-
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1223
|
-
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1224
|
-
| `POSTGRES_OPTIONS` | `dict` | `{}` | — |
|
|
1225
|
-
| `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
|
|
1226
|
-
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1227
|
-
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1228
|
-
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1229
|
-
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1246
|
+
The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL` is read as a platform-compat fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds).
|
|
1247
|
+
|
|
1248
|
+
| Setting | Type | Default | Env var |
|
|
1249
|
+
| ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
|
|
1250
|
+
| `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
|
|
1251
|
+
| `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
|
|
1252
|
+
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1253
|
+
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1254
|
+
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1255
|
+
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1256
|
+
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1257
|
+
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1230
1258
|
|
|
1231
1259
|
See [`default_settings.py`](./default_settings.py) for more details.
|
|
1232
1260
|
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.97.0](https://github.com/dropseed/plain/releases/plain-postgres@0.97.0) (2026-04-21)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **Replaced individual `POSTGRES_*` connection fields with a single `POSTGRES_URL` setting.** `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DATABASE`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_OPTIONS`, and `POSTGRES_TIME_ZONE` are gone — configure the connection with one URL (e.g. `postgresql://user:pass@host:5432/db?sslmode=require`). `DATABASE_URL` is still read as a fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds). ([770a74606463](https://github.com/dropseed/plain/commit/770a74606463))
|
|
8
|
+
- **Added `POSTGRES_MANAGEMENT_URL` for routing DDL through a separate connection.** When set, `plain migrations create|apply|list|prune|squash`, `plain postgres sync|converge|schema|diagnose|drop-unknown-tables|shell` connect through this URL instead of `POSTGRES_URL`. Use it to bypass transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, pgbouncer) for schema changes, long transactions, and `pg_dump`. A new `use_management_connection()` context manager routes custom code through the same connection. When unset, all commands use `POSTGRES_URL` — no behavior change for existing apps. ([d1cc9630d049](https://github.com/dropseed/plain/commit/d1cc9630d049))
|
|
9
|
+
- **Extracted the test-database lifecycle off `DatabaseConnection`.** Test setup/teardown now lives in `plain.postgres.test` instead of coupling it to the runtime connection class. ([ea67f82c746c](https://github.com/dropseed/plain/commit/ea67f82c746c))
|
|
10
|
+
- **Removed thin psycopg re-export wrappers.** Internal code now imports directly from `psycopg` rather than the redundant Plain-level passthroughs. ([d1cb74100e0d](https://github.com/dropseed/plain/commit/d1cb74100e0d))
|
|
11
|
+
|
|
12
|
+
### Upgrade instructions
|
|
13
|
+
|
|
14
|
+
- **Replace individual `POSTGRES_*` settings with `POSTGRES_URL`** in `app/settings.py` (or `PLAIN_POSTGRES_URL` in the environment). For example:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# Before
|
|
18
|
+
POSTGRES_HOST = "localhost"
|
|
19
|
+
POSTGRES_PORT = 5432
|
|
20
|
+
POSTGRES_DATABASE = "myapp"
|
|
21
|
+
POSTGRES_USER = "app"
|
|
22
|
+
POSTGRES_PASSWORD = "secret"
|
|
23
|
+
|
|
24
|
+
# After
|
|
25
|
+
POSTGRES_URL = "postgresql://app:secret@localhost:5432/myapp"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Apps that already set `DATABASE_URL` in the environment don't need any change.
|
|
29
|
+
|
|
30
|
+
- **If `POSTGRES_OPTIONS` or `POSTGRES_TIME_ZONE` were set**, move them into the URL as query parameters (e.g. `?application_name=web&timezone=UTC`).
|
|
31
|
+
- **If you run behind a transaction-mode pooler**, consider setting `POSTGRES_MANAGEMENT_URL` to a direct-to-Postgres connection string so `plain migrations` and `plain postgres sync` can issue DDL.
|
|
32
|
+
|
|
3
33
|
## [0.96.0](https://github.com/dropseed/plain/releases/plain-postgres@0.96.0) (2026-04-17)
|
|
4
34
|
|
|
5
35
|
### What's changed
|
|
@@ -70,26 +70,63 @@ admin_users = User.query.filter(is_admin=True)
|
|
|
70
70
|
|
|
71
71
|
## Database connection
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Configure the database with a single URL. The canonical Plain setting is `POSTGRES_URL`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# app/settings.py
|
|
77
|
+
POSTGRES_URL = "postgresql://user:password@localhost:5432/dbname"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or via environment variable:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
PLAIN_POSTGRES_URL=postgresql://user:password@localhost:5432/dbname
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Plain also reads the `DATABASE_URL` environment variable as a fallback — it's the widely-used convention for Postgres connection strings, so most hosting setups work without extra configuration:
|
|
74
87
|
|
|
75
88
|
```sh
|
|
76
89
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
77
90
|
```
|
|
78
91
|
|
|
79
|
-
|
|
92
|
+
Precedence (highest to lowest): `PLAIN_POSTGRES_URL` → `POSTGRES_URL` in `settings.py` → `DATABASE_URL` environment variable.
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
POSTGRES_PASSWORD = "password"
|
|
94
|
+
The URL supports any libpq connection parameter as a query string — for example `?sslmode=require&application_name=web&connect_timeout=10`. These are parsed and passed through to the driver.
|
|
95
|
+
|
|
96
|
+
To explicitly disable the database (e.g. during Docker builds where no database is available), set the URL to the string `none`:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
PLAIN_POSTGRES_URL=none
|
|
88
100
|
```
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
### Bypassing a connection pooler for management operations
|
|
103
|
+
|
|
104
|
+
Transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, standalone pgbouncer in transaction mode) can't run DDL, long transactions, or `pg_dump`. To work around this, set a second URL that management commands use to reach Postgres directly:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
PLAIN_POSTGRES_URL=postgresql://app@pooler:6432/myapp
|
|
108
|
+
PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
|
|
112
|
+
|
|
113
|
+
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`, `plain migrations squash`
|
|
114
|
+
- `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
|
|
115
|
+
- `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
|
|
116
|
+
|
|
117
|
+
When it's unset, all commands use `POSTGRES_URL` — there's no behavior change for existing apps.
|
|
118
|
+
|
|
119
|
+
To route custom code through the management connection, use the `use_management_connection()` context manager:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from plain.postgres import use_management_connection
|
|
123
|
+
|
|
124
|
+
with use_management_connection():
|
|
125
|
+
# Any get_connection() / ORM calls inside this block use POSTGRES_MANAGEMENT_URL.
|
|
126
|
+
run_custom_schema_change()
|
|
127
|
+
```
|
|
91
128
|
|
|
92
|
-
|
|
129
|
+
You _can_ point the two URLs at different Postgres roles — e.g. a least-privilege DML role for runtime and a DDL-capable role for management. Plain does not currently automate the grant/ownership plumbing that split requires (default privileges for newly-created tables, ownership reassignment, preflight checks that the runtime role can see the schema). If you adopt that pattern, you're responsible for wiring those up yourself.
|
|
93
130
|
|
|
94
131
|
**PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
|
|
95
132
|
|
|
@@ -1194,27 +1231,18 @@ These are static, code-level checks that catch issues before you deploy. The `di
|
|
|
1194
1231
|
|
|
1195
1232
|
## Settings
|
|
1196
1233
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
|
1204
|
-
|
|
|
1205
|
-
| `
|
|
1206
|
-
| `
|
|
1207
|
-
| `
|
|
1208
|
-
| `
|
|
1209
|
-
| `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
|
|
1210
|
-
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1211
|
-
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1212
|
-
| `POSTGRES_OPTIONS` | `dict` | `{}` | — |
|
|
1213
|
-
| `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
|
|
1214
|
-
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1215
|
-
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1216
|
-
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1217
|
-
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1234
|
+
The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL` is read as a platform-compat fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds).
|
|
1235
|
+
|
|
1236
|
+
| Setting | Type | Default | Env var |
|
|
1237
|
+
| ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
|
|
1238
|
+
| `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
|
|
1239
|
+
| `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
|
|
1240
|
+
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1241
|
+
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1242
|
+
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1243
|
+
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1244
|
+
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1245
|
+
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1218
1246
|
|
|
1219
1247
|
See [`default_settings.py`](./default_settings.py) for more details.
|
|
1220
1248
|
|
|
@@ -6,7 +6,7 @@ from . import (
|
|
|
6
6
|
# Imports that would create circular imports if sorted
|
|
7
7
|
from .base import Model
|
|
8
8
|
from .constraints import CheckConstraint, UniqueConstraint
|
|
9
|
-
from .db import get_connection
|
|
9
|
+
from .db import get_connection, use_management_connection
|
|
10
10
|
from .deletion import CASCADE, NO_ACTION, RESTRICT, SET_NULL
|
|
11
11
|
from .expressions import F
|
|
12
12
|
from .enums import TextChoices
|
|
@@ -104,6 +104,7 @@ __all__ = [
|
|
|
104
104
|
"ReverseManyToMany",
|
|
105
105
|
# From db
|
|
106
106
|
"get_connection",
|
|
107
|
+
"use_management_connection",
|
|
107
108
|
# From registry
|
|
108
109
|
"register_model",
|
|
109
110
|
"models_registry",
|
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
7
|
from ..convergence import execute_plan, plan_convergence
|
|
8
|
+
from .decorators import database_management_command
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@click.command()
|
|
@@ -14,6 +15,7 @@ from ..convergence import execute_plan, plan_convergence
|
|
|
14
15
|
is_flag=True,
|
|
15
16
|
help="Skip confirmation prompt.",
|
|
16
17
|
)
|
|
18
|
+
@database_management_command
|
|
17
19
|
def converge(yes: bool) -> None:
|
|
18
20
|
"""Fix schema mismatches between models and the database.
|
|
19
21
|
|
|
@@ -13,6 +13,7 @@ from plain.cli import register_cli
|
|
|
13
13
|
from ..db import get_connection
|
|
14
14
|
from ..dialect import quote_name
|
|
15
15
|
from .converge import converge
|
|
16
|
+
from .decorators import database_management_command
|
|
16
17
|
from .diagnose import diagnose
|
|
17
18
|
from .schema import schema
|
|
18
19
|
from .sync import sync
|
|
@@ -32,6 +33,7 @@ cli.add_command(sync)
|
|
|
32
33
|
|
|
33
34
|
@cli.command()
|
|
34
35
|
@click.argument("parameters", nargs=-1)
|
|
36
|
+
@database_management_command
|
|
35
37
|
def shell(parameters: tuple[str, ...]) -> None:
|
|
36
38
|
"""Open an interactive database shell"""
|
|
37
39
|
conn = get_connection()
|
|
@@ -67,6 +69,7 @@ def shell(parameters: tuple[str, ...]) -> None:
|
|
|
67
69
|
is_flag=True,
|
|
68
70
|
help="Skip confirmation prompt.",
|
|
69
71
|
)
|
|
72
|
+
@database_management_command
|
|
70
73
|
def drop_unknown_tables(yes: bool) -> None:
|
|
71
74
|
"""Drop all tables not associated with a Plain model"""
|
|
72
75
|
from ..introspection import get_unknown_tables
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..db import use_management_connection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def database_management_command[F: Callable[..., Any]](f: F) -> F:
|
|
11
|
+
"""Run a click command's body through `use_management_connection()`.
|
|
12
|
+
|
|
13
|
+
Apply to CLI commands that perform schema changes, migrations, or other
|
|
14
|
+
database management operations. Inside the command, `get_connection()`
|
|
15
|
+
returns a connection opened against `POSTGRES_MANAGEMENT_URL` (falling
|
|
16
|
+
back to `POSTGRES_URL` when unset).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@functools.wraps(f)
|
|
20
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
21
|
+
with use_management_connection():
|
|
22
|
+
return f(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
return wrapper # ty: ignore[invalid-return-type]
|
|
@@ -8,6 +8,7 @@ import click
|
|
|
8
8
|
|
|
9
9
|
from ..db import get_connection
|
|
10
10
|
from ..introspection import CheckItem, CheckResult, build_table_owners, run_all_checks
|
|
11
|
+
from .decorators import database_management_command
|
|
11
12
|
|
|
12
13
|
STATUS_SYMBOLS = {
|
|
13
14
|
"ok": ("✓", "green"),
|
|
@@ -182,6 +183,7 @@ def format_human(
|
|
|
182
183
|
@click.option(
|
|
183
184
|
"--all", "show_all", is_flag=True, help="Include package issues in detail"
|
|
184
185
|
)
|
|
186
|
+
@database_management_command
|
|
185
187
|
def diagnose(output_json: bool, show_all: bool) -> None:
|
|
186
188
|
"""Run health checks against the database"""
|
|
187
189
|
conn = get_connection()
|
|
@@ -27,6 +27,7 @@ from ..migrations.recorder import MigrationRecorder
|
|
|
27
27
|
from ..migrations.state import ModelState, ProjectState
|
|
28
28
|
from ..migrations.writer import MigrationWriter
|
|
29
29
|
from ..registry import models_registry
|
|
30
|
+
from .decorators import database_management_command
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
32
33
|
from ..connection import DatabaseConnection
|
|
@@ -68,6 +69,7 @@ def cli() -> None:
|
|
|
68
69
|
default=1,
|
|
69
70
|
help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
|
|
70
71
|
)
|
|
72
|
+
@database_management_command
|
|
71
73
|
def create(
|
|
72
74
|
package_labels: tuple[str, ...],
|
|
73
75
|
dry_run: bool,
|
|
@@ -345,6 +347,7 @@ def create(
|
|
|
345
347
|
is_flag=True,
|
|
346
348
|
help="Suppress migration output (used for test database creation).",
|
|
347
349
|
)
|
|
350
|
+
@database_management_command
|
|
348
351
|
def apply(
|
|
349
352
|
package_label: str | None,
|
|
350
353
|
migration_name: str | None,
|
|
@@ -638,6 +641,7 @@ def apply(
|
|
|
638
641
|
default=1,
|
|
639
642
|
help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
|
|
640
643
|
)
|
|
644
|
+
@database_management_command
|
|
641
645
|
def list_migrations(
|
|
642
646
|
package_labels: tuple[str, ...], format: str, verbosity: int
|
|
643
647
|
) -> None:
|
|
@@ -777,6 +781,7 @@ def list_migrations(
|
|
|
777
781
|
is_flag=True,
|
|
778
782
|
help="Skip confirmation prompt.",
|
|
779
783
|
)
|
|
784
|
+
@database_management_command
|
|
780
785
|
def prune(yes: bool) -> None:
|
|
781
786
|
"""Remove stale migration records from the database"""
|
|
782
787
|
# Load migrations from disk and database
|
|
@@ -899,6 +904,7 @@ def prune(yes: bool) -> None:
|
|
|
899
904
|
default=1,
|
|
900
905
|
help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
|
|
901
906
|
)
|
|
907
|
+
@database_management_command
|
|
902
908
|
def squash(
|
|
903
909
|
package_label: str,
|
|
904
910
|
start_migration_name: str | None,
|
|
@@ -10,6 +10,7 @@ from ..convergence.planning import can_auto_fix
|
|
|
10
10
|
from ..db import get_connection
|
|
11
11
|
from ..introspection import MANAGED_CONSTRAINT_TYPES, get_unknown_tables
|
|
12
12
|
from ..registry import models_registry
|
|
13
|
+
from .decorators import database_management_command
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _ok() -> None:
|
|
@@ -108,6 +109,7 @@ def _render_model(analysis: ModelAnalysis) -> None:
|
|
|
108
109
|
@click.command()
|
|
109
110
|
@click.argument("model_label", required=False)
|
|
110
111
|
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
112
|
+
@database_management_command
|
|
111
113
|
def schema(model_label: str | None, output_json: bool) -> None:
|
|
112
114
|
"""Show database schema from models, compared against the actual database"""
|
|
113
115
|
models = models_registry.get_models()
|
|
@@ -7,6 +7,7 @@ import click
|
|
|
7
7
|
from plain.runtime import settings
|
|
8
8
|
|
|
9
9
|
from ..convergence import execute_plan, plan_convergence
|
|
10
|
+
from .decorators import database_management_command
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@click.command()
|
|
@@ -15,6 +16,7 @@ from ..convergence import execute_plan, plan_convergence
|
|
|
15
16
|
is_flag=True,
|
|
16
17
|
help="Exit with non-zero status if sync would make any database changes.",
|
|
17
18
|
)
|
|
19
|
+
@database_management_command
|
|
18
20
|
def sync(check: bool) -> None:
|
|
19
21
|
"""Sync the database schema with models.
|
|
20
22
|
|