plain.postgres 0.97.0__tar.gz → 0.99.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.97.0/plain/postgres/README.md → plain_postgres-0.99.0/PKG-INFO +162 -44
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/CHANGELOG.md +63 -0
- plain_postgres-0.97.0/PKG-INFO → plain_postgres-0.99.0/plain/postgres/README.md +148 -56
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/__init__.py +3 -0
- plain_postgres-0.99.0/plain/postgres/adapters.py +41 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/core.py +16 -7
- plain_postgres-0.99.0/plain/postgres/cli/diagnose.py +312 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/config.py +4 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/connection.py +39 -303
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/database_url.py +36 -9
- plain_postgres-0.99.0/plain/postgres/db.py +158 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/default_settings.py +7 -3
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/introspection/__init__.py +2 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/__init__.py +34 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/checks_cumulative.py +670 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/checks_snapshot.py +330 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/checks_structural.py +292 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/context.py +223 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/helpers.py +164 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/ownership.py +65 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/runner.py +158 -0
- plain_postgres-0.99.0/plain/postgres/introspection/health/types.py +54 -0
- plain_postgres-0.99.0/plain/postgres/middleware.py +37 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/otel.py +192 -11
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/preflight.py +31 -1
- plain_postgres-0.99.0/plain/postgres/sources.py +221 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/database.py +47 -24
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/pytest.py +26 -19
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/transaction.py +8 -12
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/utils.py +22 -8
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/pyproject.toml +3 -3
- plain_postgres-0.99.0/tests/conftest.py +11 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_connection_isolation.py +16 -16
- plain_postgres-0.99.0/tests/test_connection_lifecycle.py +383 -0
- plain_postgres-0.99.0/tests/test_connection_pool.py +162 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_timeouts.py +2 -1
- plain_postgres-0.99.0/tests/test_diagnose.py +244 -0
- plain_postgres-0.99.0/tests/test_executor_connection_hook.py +103 -0
- plain_postgres-0.99.0/tests/test_health.py +47 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_management_connection.py +3 -3
- plain_postgres-0.99.0/tests/test_otel_metrics.py +243 -0
- plain_postgres-0.99.0/tests/test_read_only_transactions.py +87 -0
- plain_postgres-0.97.0/plain/postgres/cli/diagnose.py +0 -206
- plain_postgres-0.97.0/plain/postgres/connections.py +0 -126
- plain_postgres-0.97.0/plain/postgres/db.py +0 -38
- plain_postgres-0.97.0/plain/postgres/introspection/health.py +0 -737
- plain_postgres-0.97.0/tests/test_connection_lifecycle.py +0 -354
- plain_postgres-0.97.0/tests/test_read_only_transactions.py +0 -116
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/.gitignore +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/CLAUDE.md +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/LICENSE +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/README.md +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_constraints.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_defaults.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_fk.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_indexes.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_nullability.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_field_defaults.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_functions_uuid.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_introspection.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_iterator.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_m2m.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_migration_executor.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_mixins.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_random_string_field.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_raw_query.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_related.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_schema_timeouts.py +0 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.postgres
|
|
3
|
+
Version: 0.99.0
|
|
4
|
+
Summary: Model your data and store it in a database.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: plain<1.0.0,>=0.134.0
|
|
10
|
+
Requires-Dist: psycopg-pool>=3.2
|
|
11
|
+
Requires-Dist: psycopg>=3.2
|
|
12
|
+
Requires-Dist: sqlparse>=0.3.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# plain.postgres
|
|
2
16
|
|
|
3
17
|
**Model your data and store it in a database.**
|
|
4
18
|
|
|
5
19
|
- [Overview](#overview)
|
|
6
20
|
- [Database connection](#database-connection)
|
|
21
|
+
- [Middleware](#middleware)
|
|
22
|
+
- [Bypassing a connection pooler for management operations](#bypassing-a-connection-pooler-for-management-operations)
|
|
7
23
|
- [Querying](#querying)
|
|
8
24
|
- [Schema management](#schema-management)
|
|
9
25
|
- [Syncing](#syncing)
|
|
@@ -99,6 +115,24 @@ To explicitly disable the database (e.g. during Docker builds where no database
|
|
|
99
115
|
PLAIN_POSTGRES_URL=none
|
|
100
116
|
```
|
|
101
117
|
|
|
118
|
+
### Middleware
|
|
119
|
+
|
|
120
|
+
Connections are checked out lazily on first use and returned to the pool when the HTTP request finishes. That's handled by [`DatabaseConnectionMiddleware`](./middleware.py#DatabaseConnectionMiddleware) — add it to `MIDDLEWARE` once you install `plain.postgres`:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# app/settings.py
|
|
124
|
+
MIDDLEWARE = [
|
|
125
|
+
"plain.postgres.DatabaseConnectionMiddleware",
|
|
126
|
+
# ...other middleware
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Place it near the top so downstream middleware can use the database inside `before_request` / `after_response` and still have the connection returned cleanly at the end.
|
|
131
|
+
|
|
132
|
+
For `StreamingResponse` / `AsyncStreamingResponse`, the connection is returned after the body is fully drained (not when the view returns), so generators that lazily query the database — for example `Model.query.iterator()` or raw cursor loops — keep their cursor alive until the last chunk is sent.
|
|
133
|
+
|
|
134
|
+
Without the middleware, connections keep living on their thread until something explicitly calls `plain.postgres.db.return_database_connection()` (or the process exits). That's fine for short-lived scripts but wastes a connection per thread in long-running servers.
|
|
135
|
+
|
|
102
136
|
### Bypassing a connection pooler for management operations
|
|
103
137
|
|
|
104
138
|
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:
|
|
@@ -128,14 +162,6 @@ with use_management_connection():
|
|
|
128
162
|
|
|
129
163
|
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.
|
|
130
164
|
|
|
131
|
-
**PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
uv add psycopg[binary] # Pre-built wheels, easiest for local development
|
|
135
|
-
# or
|
|
136
|
-
uv add psycopg[c] # Compiled against your system's libpq, recommended for production
|
|
137
|
-
```
|
|
138
|
-
|
|
139
165
|
## Querying
|
|
140
166
|
|
|
141
167
|
Models come with a powerful query API through their [`QuerySet`](./query.py#QuerySet) interface:
|
|
@@ -443,32 +469,34 @@ with transaction.atomic():
|
|
|
443
469
|
safe_operation() # This still runs in the outer transaction
|
|
444
470
|
```
|
|
445
471
|
|
|
446
|
-
### Read-only
|
|
472
|
+
### Read-only transactions
|
|
447
473
|
|
|
448
|
-
|
|
474
|
+
Run a block of code in a read-only transaction using `read_only()`. Any write (INSERT, UPDATE, DELETE, DDL) raises `psycopg.errors.ReadOnlySqlTransaction`:
|
|
449
475
|
|
|
450
476
|
```python
|
|
451
|
-
from plain.postgres.
|
|
477
|
+
from plain.postgres.db import read_only
|
|
452
478
|
|
|
453
479
|
with read_only():
|
|
454
480
|
users = User.query.all() # reads work
|
|
455
481
|
User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
|
|
456
482
|
```
|
|
457
483
|
|
|
458
|
-
|
|
484
|
+
`read_only()` opens a single `BEGIN READ ONLY` transaction for the block. Nested `atomic()` blocks inside become savepoints of the outer read-only transaction and inherit read-only.
|
|
459
485
|
|
|
460
|
-
|
|
486
|
+
Because it opens its own transaction, `read_only()` cannot be entered inside an existing `atomic()` block — doing so raises `TransactionManagementError`.
|
|
461
487
|
|
|
462
|
-
|
|
463
|
-
from plain.postgres.db import get_connection
|
|
488
|
+
Because the whole block is one transaction, catching a database error inside `read_only()` and trying to keep reading will fail — the transaction is aborted and any further query raises `TransactionManagementError`. Wrap the write in a nested `atomic()` savepoint if you need to recover and continue:
|
|
464
489
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
490
|
+
```python
|
|
491
|
+
with read_only():
|
|
492
|
+
try:
|
|
493
|
+
with atomic():
|
|
494
|
+
User.query.create(name="x") # raises, savepoint rolls back
|
|
495
|
+
except psycopg.errors.ReadOnlySqlTransaction:
|
|
496
|
+
pass
|
|
497
|
+
User.query.count() # still works — outer txn is healthy
|
|
468
498
|
```
|
|
469
499
|
|
|
470
|
-
Read-only mode must be set outside a transaction — calling it inside `atomic()` raises `TransactionManagementError`.
|
|
471
|
-
|
|
472
500
|
## Schema management
|
|
473
501
|
|
|
474
502
|
Schema changes fall into three categories, each with a different author and apply model:
|
|
@@ -1170,37 +1198,110 @@ graph TB
|
|
|
1170
1198
|
|
|
1171
1199
|
## Diagnostics
|
|
1172
1200
|
|
|
1173
|
-
|
|
1201
|
+
Run health checks against your database. `diagnose` is designed to produce
|
|
1202
|
+
only actionable findings — every warning has a copy-paste fix or specific
|
|
1203
|
+
resource to investigate, and noisy one-off signals (hit ratios, XID age) are
|
|
1204
|
+
surfaced as informational context rather than as warnings.
|
|
1174
1205
|
|
|
1175
1206
|
```bash
|
|
1176
1207
|
uv run plain postgres diagnose
|
|
1177
1208
|
```
|
|
1178
1209
|
|
|
1179
|
-
|
|
1210
|
+
Output modes:
|
|
1180
1211
|
|
|
1181
1212
|
```bash
|
|
1182
|
-
uv run plain postgres diagnose --json
|
|
1213
|
+
uv run plain postgres diagnose --json # structured output for scripts/agents
|
|
1214
|
+
uv run plain postgres diagnose --verbose # expand to show every check, including passing
|
|
1215
|
+
uv run plain postgres diagnose --all # include findings on installed-package tables
|
|
1183
1216
|
```
|
|
1184
1217
|
|
|
1185
|
-
|
|
1218
|
+
### Guiding principle
|
|
1186
1219
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1220
|
+
`diagnose` emits a **warning** only if the remedy fits in the user's codebase
|
|
1221
|
+
or is an app-level action they own. If the remedy is "run SQL against your
|
|
1222
|
+
DB" or "configure your Postgres server," the check emits **operational
|
|
1223
|
+
context** or an **informational number**, not a warning. This keeps the
|
|
1224
|
+
warning surface high-trust — every warning has an edit-to-make — and prevents
|
|
1225
|
+
`diagnose` from bleeding into DB-host concerns.
|
|
1226
|
+
|
|
1227
|
+
### Warning-tier checks
|
|
1228
|
+
|
|
1229
|
+
Things the user can fix by editing code + running `plain postgres sync`, or
|
|
1230
|
+
app-level incidents they must act on.
|
|
1231
|
+
|
|
1232
|
+
**Structural — always-real; a fix is possible immediately.**
|
|
1233
|
+
|
|
1234
|
+
| Check | What it finds | Severity |
|
|
1235
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------- | ---------------- |
|
|
1236
|
+
| **Invalid indexes** | Broken indexes from failed `CREATE INDEX CONCURRENTLY` — maintained on writes, never used for reads | Warning |
|
|
1237
|
+
| **Duplicate indexes** | One index is a column-prefix of another on the same table | Warning |
|
|
1238
|
+
| **Missing FK indexes** | Foreign key columns without any index coverage | Warning |
|
|
1239
|
+
| **Sequence exhaustion** | Identity sequences approaching their type max | Warning/Critical |
|
|
1190
1240
|
|
|
1191
|
-
|
|
1241
|
+
**Cumulative — depends on stats since the last reset.**
|
|
1192
1242
|
|
|
1193
|
-
| Check
|
|
1194
|
-
|
|
|
1195
|
-
| **
|
|
1196
|
-
| **
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
|
1201
|
-
|
|
|
1202
|
-
| **
|
|
1203
|
-
| **
|
|
1243
|
+
| Check | What it finds | Severity |
|
|
1244
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
1245
|
+
| **Unused indexes** | Indexes with zero scans since stats reset (>1 MB). Excludes unique, constraint-backing, and sole-FK-coverage indexes | Warning |
|
|
1246
|
+
| **Missing index candidates** | Tables with seq-scan activity suggesting a missing index. Includes top contributing queries from pg_stat_statements | Warning |
|
|
1247
|
+
|
|
1248
|
+
**Snapshot — point-in-time incidents.**
|
|
1249
|
+
|
|
1250
|
+
| Check | What it finds | Severity |
|
|
1251
|
+
| ---------------------------- | ----------------------------------------------------------------------- | ---------------- |
|
|
1252
|
+
| **Long-running connections** | Client backends idle-in-transaction or running a query past a threshold | Warning/Critical |
|
|
1253
|
+
| **Blocking queries** | Queries currently blocking other queries via held locks | Warning/Critical |
|
|
1254
|
+
|
|
1255
|
+
### Operational-context findings
|
|
1256
|
+
|
|
1257
|
+
These are facts about the database whose remedies live outside Plain today
|
|
1258
|
+
(`ANALYZE`, `VACUUM`, `REINDEX`, autovacuum server tuning). They're surfaced
|
|
1259
|
+
so agents and humans can interpret findings correctly, but the CLI renders
|
|
1260
|
+
them as context rather than alarming warnings — the user can't express the
|
|
1261
|
+
fix in their model code. (In JSON output each finding still carries
|
|
1262
|
+
`status: "warning"`; the `tier: "operational"` field is what distinguishes
|
|
1263
|
+
it.) Each finding still carries the exact SQL in its suggestion for anyone
|
|
1264
|
+
who wants to act.
|
|
1265
|
+
|
|
1266
|
+
| Finding | What it reports |
|
|
1267
|
+
| ------------------- | -------------------------------------------------------------------------------- |
|
|
1268
|
+
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
|
|
1269
|
+
| **Vacuum health** | Tables with >10% dead tuples |
|
|
1270
|
+
| **Index bloat** | btree indexes with significant estimated wasted space (≥10 MB, ioguix estimator) |
|
|
1271
|
+
|
|
1272
|
+
If a future release exposes per-table autovacuum / fillfactor parameters in
|
|
1273
|
+
`model_options` (see the `postgres-model-storage-parameters` arc), these
|
|
1274
|
+
findings can graduate back to the warning tier — because the remedy will be
|
|
1275
|
+
expressible in code.
|
|
1276
|
+
|
|
1277
|
+
### Informational context
|
|
1278
|
+
|
|
1279
|
+
Alongside checks, `diagnose` surfaces context an agent or human may want to read but that isn't actionable on its own:
|
|
1280
|
+
|
|
1281
|
+
- **Cache hit ratio**, **Index hit ratio** — buffer hit rates (volatile after restart; not a warning in themselves)
|
|
1282
|
+
- **XID wraparound** — transaction ID age as a percent of the 2B limit. Autovacuum usually keeps this low; long-running transactions can block the freeze process even on managed Postgres
|
|
1283
|
+
- **Connection saturation** — active/max connections at this moment
|
|
1284
|
+
- **Stats reset** — when cumulative stats were last reset (affects the confidence of operational checks)
|
|
1285
|
+
- **pg_stat_statements** — whether the extension is installed
|
|
1286
|
+
|
|
1287
|
+
### Cross-check caveats
|
|
1288
|
+
|
|
1289
|
+
Findings whose confidence depends on another check are tagged with a caveat. For example:
|
|
1290
|
+
|
|
1291
|
+
- `unused_indexes` on a table flagged by `stats_freshness` → caveat: "this table has never been analyzed — the planner may not yet use this index; re-check after running ANALYZE"
|
|
1292
|
+
- `missing_index_candidates` on a never-analyzed table → caveat: "planner statistics are absent — running ANALYZE may change query plans and make this finding moot"
|
|
1293
|
+
|
|
1294
|
+
This prevents false confidence: dropping an "unused" index on a never-analyzed table is often the wrong move.
|
|
1295
|
+
|
|
1296
|
+
### Model-aware suggestions
|
|
1297
|
+
|
|
1298
|
+
Findings on app-owned tables include the Plain model class and its source file. Suggestions reference the exact edit point:
|
|
1299
|
+
|
|
1300
|
+
```
|
|
1301
|
+
app/processing/models.py :: ProcessingResult — Add an Index on ["is_processing"] to the model, then run plain postgres sync
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
This closes the loop from detection to fix — agents can draft the model edit without guessing.
|
|
1204
1305
|
|
|
1205
1306
|
### App vs package issues
|
|
1206
1307
|
|
|
@@ -1220,6 +1321,8 @@ heroku run -a your-app "plain postgres diagnose --json"
|
|
|
1220
1321
|
|
|
1221
1322
|
The `--json` flag must be quoted so Heroku passes it through to the command.
|
|
1222
1323
|
|
|
1324
|
+
Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
|
|
1325
|
+
|
|
1223
1326
|
### Preflight checks
|
|
1224
1327
|
|
|
1225
1328
|
Two related checks run automatically during `uv run plain preflight` (and `uv run plain check`):
|
|
@@ -1229,6 +1332,12 @@ Two related checks run automatically during `uv run plain preflight` (and `uv ru
|
|
|
1229
1332
|
|
|
1230
1333
|
These are static, code-level checks that catch issues before you deploy. The `diagnose` command complements them with runtime stats from the actual database.
|
|
1231
1334
|
|
|
1335
|
+
### What diagnose deliberately doesn't do
|
|
1336
|
+
|
|
1337
|
+
- **LLM-powered column recommendations for missing indexes** — `missing_index_candidates` shows the culprit queries and lets you decide. For precise column-level suggestions, use a platform tool (PlanetScale Insights, Dexter, pg_qualstats + hypopg).
|
|
1338
|
+
- **Historical trending** — `diagnose` is stateless; it reports on the current state of cumulative stats. Continuous monitoring is out of scope.
|
|
1339
|
+
- **Niche server checks** (WAL bloat, replication slot age, etc.) — better covered by your Postgres provider's monitoring or a dedicated tool; users on self-hosted setups that need them typically have their own tooling.
|
|
1340
|
+
|
|
1232
1341
|
## Settings
|
|
1233
1342
|
|
|
1234
1343
|
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).
|
|
@@ -1237,8 +1346,10 @@ The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL`
|
|
|
1237
1346
|
| ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
|
|
1238
1347
|
| `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
|
|
1239
1348
|
| `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
|
|
1240
|
-
| `
|
|
1241
|
-
| `
|
|
1349
|
+
| `POSTGRES_POOL_MIN_SIZE` | `int` | `4` | `PLAIN_POSTGRES_POOL_MIN_SIZE` |
|
|
1350
|
+
| `POSTGRES_POOL_MAX_SIZE` | `int` | `20` | `PLAIN_POSTGRES_POOL_MAX_SIZE` |
|
|
1351
|
+
| `POSTGRES_POOL_MAX_LIFETIME` | `float` | `3600.0` | `PLAIN_POSTGRES_POOL_MAX_LIFETIME` |
|
|
1352
|
+
| `POSTGRES_POOL_TIMEOUT` | `float` | `30.0` | `PLAIN_POSTGRES_POOL_TIMEOUT` |
|
|
1242
1353
|
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1243
1354
|
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1244
1355
|
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
@@ -1282,13 +1393,15 @@ Currently, Plain supports a single database connection per application. For appl
|
|
|
1282
1393
|
|
|
1283
1394
|
## Installation
|
|
1284
1395
|
|
|
1285
|
-
Install the `plain.postgres` package from [PyPI](https://pypi.org/project/plain.postgres/)
|
|
1396
|
+
Install the `plain.postgres` package from [PyPI](https://pypi.org/project/plain.postgres/). You must also pick a `psycopg` implementation — `plain.postgres` depends on `psycopg` but does not pick one for you, so installing `plain.postgres` alone will not be able to connect.
|
|
1286
1397
|
|
|
1287
1398
|
```bash
|
|
1288
|
-
uv add plain.postgres psycopg[binary]
|
|
1399
|
+
uv add plain.postgres psycopg[binary] # Pre-built wheels, easiest for local development
|
|
1400
|
+
# or
|
|
1401
|
+
uv add plain.postgres psycopg[c] # Compiled against your system's libpq, recommended for production
|
|
1289
1402
|
```
|
|
1290
1403
|
|
|
1291
|
-
Then add to your `INSTALLED_PACKAGES
|
|
1404
|
+
Then add to your `INSTALLED_PACKAGES` and register [`DatabaseConnectionMiddleware`](#middleware) so pooled connections are returned at the end of each request:
|
|
1292
1405
|
|
|
1293
1406
|
```python
|
|
1294
1407
|
# app/settings.py
|
|
@@ -1296,4 +1409,9 @@ INSTALLED_PACKAGES = [
|
|
|
1296
1409
|
...
|
|
1297
1410
|
"plain.postgres",
|
|
1298
1411
|
]
|
|
1412
|
+
|
|
1413
|
+
MIDDLEWARE = [
|
|
1414
|
+
"plain.postgres.DatabaseConnectionMiddleware",
|
|
1415
|
+
...
|
|
1416
|
+
]
|
|
1299
1417
|
```
|
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.99.0](https://github.com/dropseed/plain/releases/plain-postgres@0.99.0) (2026-04-23)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **Reworked `plain postgres diagnose` around tiered findings.** Warnings are now reserved for things the user can fix by editing model code or taking an app-level action — every warning carries a copy-paste fix or a model-file pointer (`app/path.py :: ModelName`). Noisy one-off signals (cache/index hit ratios, XID wraparound, connection saturation, pg_stat_statements availability, stats reset age) render as **informational context**; DB-state facts whose remedies live outside Plain (stats freshness, vacuum health, index bloat) render as **operational context** instead of warnings. Added `--verbose` to expand every check, and `--all` still includes installed-package tables. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
|
|
8
|
+
- **New diagnostic checks:** `stats_freshness` (uses `pg_class.reltuples` so it survives `pg_stat_reset`), `index_bloat` (ioguix btree estimator, public schema only), `missing_index_candidates` (seq-scan heuristics with per-query drill-down from `pg_stat_statements`), `blocking_queries` (wait age from `pg_locks.waitstart`, PG 14+), and `long_running_connections` (xact age for idle-in-transaction). Findings include **cross-check caveats** — e.g. an `unused_indexes` finding on a table that's also flagged by `stats_freshness` or `vacuum_health` now carries a warning that dropping the index may be premature. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
|
|
9
|
+
- **Permission-safe probes.** Checks that may hit permission errors (`pg_stat_statements`, `pg_stat_activity`, `pg_locks`) now wrap their queries in `cursor.connection.transaction()` so a failure rolls back cleanly in either autocommit or transaction mode without cascade-failing later checks. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
|
|
10
|
+
- **Refactored internals.** The 1800+ line `introspection/health.py` split into an `introspection/health/` package along natural seams (types, ownership, context, helpers, checks grouped by `structural`/`cumulative`/`snapshot`, and a runner). Public re-exports are unchanged. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
|
|
11
|
+
- Adapter annotations use `Response` after plain 0.135.0 merged `ResponseBase` into `Response`. ([f5007281d7fa](https://github.com/dropseed/plain/commit/f5007281d7fa))
|
|
12
|
+
|
|
13
|
+
### Upgrade instructions
|
|
14
|
+
|
|
15
|
+
- Requires `plain>=0.135.0`.
|
|
16
|
+
- No code changes required. If you parse `plain postgres diagnose --json`, note the new `tier` field on each finding (`"structural"`, `"cumulative"`, `"snapshot"`, or `"operational"`) — operational findings still carry `status: "warning"` but the CLI renders them as context rather than as alarming warnings.
|
|
17
|
+
|
|
18
|
+
## [0.98.0](https://github.com/dropseed/plain/releases/plain-postgres@0.98.0) (2026-04-22)
|
|
19
|
+
|
|
20
|
+
### What's changed
|
|
21
|
+
|
|
22
|
+
- **Pool-backed connections via `psycopg_pool.ConnectionPool`.** A new `sources` abstraction routes `DatabaseConnection` through either a long-lived `PoolSource` (runtime) or a `DirectSource` (management / one-shot). Each request checks a connection out of the pool on first use and returns it when the HTTP request finishes. `psycopg>=3.2` and `psycopg-pool>=3.2` are now declared as hard dependencies. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
|
|
23
|
+
- **New `DatabaseConnectionMiddleware` (required).** Add `"plain.postgres.DatabaseConnectionMiddleware"` to `MIDDLEWARE` — it's what returns the pooled connection at the end of each request. For `StreamingResponse` / `AsyncStreamingResponse` the connection is returned after the body fully drains, so generators that lazily query the database (e.g. `Model.query.iterator()`) keep their cursor alive until the last chunk is sent. A new `postgres.middleware_installed` preflight check errors if the middleware is missing. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
|
|
24
|
+
- **Connection settings replaced with pool settings.** `POSTGRES_CONN_MAX_AGE` and `POSTGRES_CONN_HEALTH_CHECKS` are gone. Tune the pool with `POSTGRES_POOL_MIN_SIZE` (default `4`), `POSTGRES_POOL_MAX_SIZE` (default `20`), `POSTGRES_POOL_MAX_LIFETIME` seconds (default `3600.0`), and `POSTGRES_POOL_TIMEOUT` seconds (default `30.0`). Each is also available as a `PLAIN_POSTGRES_POOL_*` environment variable. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
|
|
25
|
+
- **`plain.postgres.connections` module removed.** `get_connection`, `has_connection`, `use_management_connection`, and `read_only` now live in `plain.postgres.db` (the underscore-less counterpart). ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
|
|
26
|
+
- **`read_only()` is now pgbouncer-safe.** It opens a single `BEGIN READ ONLY` transaction for the block (previously a session-level `SET default_transaction_read_only = on`). Nested `atomic()` blocks become savepoints of the outer read-only transaction. Entering `read_only()` inside an existing `atomic()` block now raises `TransactionManagementError`. The old `DatabaseConnection.set_read_only()` method is removed. ([ebdec30](https://github.com/dropseed/plain/commit/ebdec30))
|
|
27
|
+
- **Added OTel pool + rowcount metrics and semconv polish.** Wires the `db.client.connection.*` metric family (count, max, idle.min/max, pending_requests, wait_time, use_time, timeouts) from the pool's stats and the acquire/release path, plus `db.client.response.returned_rows` for SELECT queries including streamed iterators. Query spans now carry `server.address` / `server.port` alongside `network.peer.*`, and the tracer/meter are tagged with the `plain.postgres` package version for `InstrumentationScope`. ([61278d5](https://github.com/dropseed/plain/commit/61278d5))
|
|
28
|
+
- **Moved `psql` CLI orchestration off `DatabaseConnection`.** New `postgres_cli_args` / `postgres_cli_env` helpers in `plain.postgres.database_url` build the arguments and environment for `psql`, `pg_dump`, etc.; `plain postgres shell` and the `plain-dev` backup client both use them. `DatabaseConnection.runshell()` and `executable_name` are gone. ([5b4a488](https://github.com/dropseed/plain/commit/5b4a488))
|
|
29
|
+
- **Removed dead connection-lifecycle plumbing.** `close_if_unusable_or_obsolete`, `close_if_health_check_failed`, `closed_in_transaction`, `is_usable`, `health_check_enabled`, `health_check_done`, `close_at`, `_maintenance_cursor`, and `DatabaseConnection.from_url` are gone — the pool handles recycling, health checks, and URL parsing. `close()` now validates there's no open atomic block instead of silently deferring. ([044e942](https://github.com/dropseed/plain/commit/044e942), [2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
|
|
30
|
+
- **Inlined `pg_version` and removed `temporary_connection()`.** The single caller now reads `connection.info.server_version` directly; `temporary_connection()` has no remaining users. ([319f6ac](https://github.com/dropseed/plain/commit/319f6ac))
|
|
31
|
+
- **`APIResult` shorthand returns moved out of `View`.** Any internal views that relied on dict/int shorthand now wrap their returns in `JsonResponse` / `Response(status_code=...)` to match plain 0.134.0's narrower `View` handler return type. ([1935f3f](https://github.com/dropseed/plain/commit/1935f3f))
|
|
32
|
+
- **Adapter registration extracted to `plain.postgres.adapters`.** `PlainRangeDumper` and `get_adapters_template()` moved out of `connection.py` into their own module.
|
|
33
|
+
|
|
34
|
+
### Upgrade instructions
|
|
35
|
+
|
|
36
|
+
- Requires `plain>=0.134.0`.
|
|
37
|
+
- **Add the middleware** to `app/settings.py`:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
MIDDLEWARE = [
|
|
41
|
+
"plain.postgres.DatabaseConnectionMiddleware",
|
|
42
|
+
# ...the rest of your middleware
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Place it near the top so downstream middleware can use the database inside `before_request` / `after_response` and still have the connection returned cleanly. Preflight will error if it's missing.
|
|
47
|
+
|
|
48
|
+
- **Replace `POSTGRES_CONN_MAX_AGE` / `POSTGRES_CONN_HEALTH_CHECKS`** with the pool settings (`POSTGRES_POOL_MIN_SIZE`, `POSTGRES_POOL_MAX_SIZE`, `POSTGRES_POOL_MAX_LIFETIME`, `POSTGRES_POOL_TIMEOUT`) or remove them to take the defaults.
|
|
49
|
+
|
|
50
|
+
- **Update imports from `plain.postgres.connections`** to `plain.postgres.db`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# Before
|
|
54
|
+
from plain.postgres.connections import get_connection, read_only, use_management_connection
|
|
55
|
+
|
|
56
|
+
# After
|
|
57
|
+
from plain.postgres.db import get_connection, read_only, use_management_connection
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **If you called `DatabaseConnection.set_read_only(True)`** for a sticky read-only session, switch to the `read_only()` context manager around the block you want read-only. If you need session-level enforcement outside a transaction, open a `DirectSource` connection yourself and issue `SET default_transaction_read_only = on` on it.
|
|
61
|
+
|
|
62
|
+
- **If you entered `read_only()` inside an `atomic()` block**, move `read_only()` to the outer position — it now owns the transaction. Nested `atomic()` blocks inside `read_only()` are fine (they become savepoints).
|
|
63
|
+
|
|
64
|
+
- **If you pinned `psycopg` via your own dependency**, make sure it's `>=3.2`, and add `psycopg-pool>=3.2` if you were installing psycopg without extras.
|
|
65
|
+
|
|
3
66
|
## [0.97.0](https://github.com/dropseed/plain/releases/plain-postgres@0.97.0) (2026-04-21)
|
|
4
67
|
|
|
5
68
|
### What's changed
|