plain.postgres 0.84.0__py3-none-any.whl
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/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
plain/postgres/cli/db.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from plain.cli import register_cli
|
|
11
|
+
|
|
12
|
+
from ..backups.cli import cli as backups_cli
|
|
13
|
+
from ..db import OperationalError, get_connection
|
|
14
|
+
from ..dialect import quote_name
|
|
15
|
+
from ..migrations.recorder import MIGRATION_TABLE_NAME
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_cli("db")
|
|
19
|
+
@click.group()
|
|
20
|
+
def cli() -> None:
|
|
21
|
+
"""Database operations"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
cli.add_command(backups_cli)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.command()
|
|
28
|
+
@click.argument("parameters", nargs=-1)
|
|
29
|
+
def shell(parameters: tuple[str, ...]) -> None:
|
|
30
|
+
"""Open an interactive database shell"""
|
|
31
|
+
conn = get_connection()
|
|
32
|
+
try:
|
|
33
|
+
conn.runshell(list(parameters))
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
# Note that we're assuming the FileNotFoundError relates to the
|
|
36
|
+
# command missing. It could be raised for some other reason, in
|
|
37
|
+
# which case this error message would be inaccurate. Still, this
|
|
38
|
+
# message catches the common case.
|
|
39
|
+
click.secho(
|
|
40
|
+
f"You appear not to have the {conn.executable_name!r} program installed or on your path.",
|
|
41
|
+
fg="red",
|
|
42
|
+
err=True,
|
|
43
|
+
)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
except subprocess.CalledProcessError as e:
|
|
46
|
+
click.secho(
|
|
47
|
+
'"{}" returned non-zero exit status {}.'.format(
|
|
48
|
+
" ".join(e.cmd),
|
|
49
|
+
e.returncode,
|
|
50
|
+
),
|
|
51
|
+
fg="red",
|
|
52
|
+
err=True,
|
|
53
|
+
)
|
|
54
|
+
sys.exit(e.returncode)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@cli.command("drop-unknown-tables")
|
|
58
|
+
@click.option(
|
|
59
|
+
"--yes",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
help="Skip confirmation prompt (for non-interactive use).",
|
|
62
|
+
)
|
|
63
|
+
def drop_unknown_tables(yes: bool) -> None:
|
|
64
|
+
"""Drop all tables not associated with a Plain model"""
|
|
65
|
+
conn = get_connection()
|
|
66
|
+
db_tables = set(conn.table_names())
|
|
67
|
+
model_tables = set(conn.plain_table_names())
|
|
68
|
+
unknown_tables = sorted(db_tables - model_tables - {MIGRATION_TABLE_NAME})
|
|
69
|
+
|
|
70
|
+
if not unknown_tables:
|
|
71
|
+
click.echo("No unknown tables found.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
unknown_set = set(unknown_tables)
|
|
75
|
+
table_count = len(unknown_tables)
|
|
76
|
+
tables_label = f"{table_count} table{'s' if table_count != 1 else ''}"
|
|
77
|
+
|
|
78
|
+
# Find foreign key constraints from kept tables that reference unknown tables
|
|
79
|
+
cascade_warnings: defaultdict[str, list[tuple[str, str]]] = defaultdict(list)
|
|
80
|
+
with conn.cursor() as cursor:
|
|
81
|
+
for table in unknown_tables:
|
|
82
|
+
cursor.execute(
|
|
83
|
+
"""
|
|
84
|
+
SELECT conname, conrelid::regclass
|
|
85
|
+
FROM pg_constraint
|
|
86
|
+
WHERE confrelid = %s::regclass AND contype = 'f'
|
|
87
|
+
""",
|
|
88
|
+
[table],
|
|
89
|
+
)
|
|
90
|
+
for constraint_name, referencing_table in cursor.fetchall():
|
|
91
|
+
if str(referencing_table) not in unknown_set:
|
|
92
|
+
cascade_warnings[table].append(
|
|
93
|
+
(constraint_name, str(referencing_table))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
click.secho("Unknown tables:", fg="yellow", bold=True)
|
|
97
|
+
for table in unknown_tables:
|
|
98
|
+
click.echo(f" - {table}")
|
|
99
|
+
for constraint_name, referencing_table in cascade_warnings[table]:
|
|
100
|
+
click.secho(
|
|
101
|
+
f" ⚠ CASCADE will drop constraint {constraint_name} on {referencing_table}",
|
|
102
|
+
fg="red",
|
|
103
|
+
)
|
|
104
|
+
click.echo()
|
|
105
|
+
|
|
106
|
+
if not yes:
|
|
107
|
+
if not click.confirm(f"Drop {tables_label} (CASCADE)? This cannot be undone."):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
with conn.cursor() as cursor:
|
|
111
|
+
for table in unknown_tables:
|
|
112
|
+
click.echo(f" Dropping {table}...", nl=False)
|
|
113
|
+
cursor.execute(f"DROP TABLE IF EXISTS {quote_name(table)} CASCADE")
|
|
114
|
+
click.echo(" OK")
|
|
115
|
+
|
|
116
|
+
click.secho(f"✓ Dropped {tables_label}.", fg="green")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@cli.command()
|
|
120
|
+
def wait() -> None:
|
|
121
|
+
"""Wait for the database to be ready"""
|
|
122
|
+
attempts = 0
|
|
123
|
+
while True:
|
|
124
|
+
attempts += 1
|
|
125
|
+
waiting_for = False
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
get_connection().ensure_connection()
|
|
129
|
+
except OperationalError:
|
|
130
|
+
waiting_for = True
|
|
131
|
+
|
|
132
|
+
if waiting_for:
|
|
133
|
+
if attempts > 1:
|
|
134
|
+
# After the first attempt, start printing them
|
|
135
|
+
click.secho(
|
|
136
|
+
f"Waiting for database (attempt {attempts})",
|
|
137
|
+
fg="yellow",
|
|
138
|
+
)
|
|
139
|
+
time.sleep(1.5)
|
|
140
|
+
else:
|
|
141
|
+
click.secho("✔ Database ready", fg="green")
|
|
142
|
+
break
|