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
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .base import Operation
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from plain.postgres.migrations.state import ProjectState
|
|
10
|
+
from plain.postgres.schema import DatabaseSchemaEditor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SeparateDatabaseAndState(Operation):
|
|
14
|
+
"""
|
|
15
|
+
Take two lists of operations - ones that will be used for the database,
|
|
16
|
+
and ones that will be used for the state change. This allows operations
|
|
17
|
+
that don't support state change to have it applied, or have operations
|
|
18
|
+
that affect the state or not the database, or so on.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
serialization_expand_args = ["database_operations", "state_operations"]
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
database_operations: list[Operation] | None = None,
|
|
26
|
+
state_operations: list[Operation] | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.database_operations = database_operations or []
|
|
29
|
+
self.state_operations = state_operations or []
|
|
30
|
+
|
|
31
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, list[Operation]]]:
|
|
32
|
+
kwargs: dict[str, list[Operation]] = {}
|
|
33
|
+
if self.database_operations:
|
|
34
|
+
kwargs["database_operations"] = self.database_operations
|
|
35
|
+
if self.state_operations:
|
|
36
|
+
kwargs["state_operations"] = self.state_operations
|
|
37
|
+
return (self.__class__.__qualname__, (), kwargs)
|
|
38
|
+
|
|
39
|
+
def state_forwards(self, package_label: str, state: ProjectState) -> None:
|
|
40
|
+
for state_operation in self.state_operations:
|
|
41
|
+
state_operation.state_forwards(package_label, state)
|
|
42
|
+
|
|
43
|
+
def database_forwards(
|
|
44
|
+
self,
|
|
45
|
+
package_label: str,
|
|
46
|
+
schema_editor: DatabaseSchemaEditor,
|
|
47
|
+
from_state: ProjectState,
|
|
48
|
+
to_state: ProjectState,
|
|
49
|
+
) -> None:
|
|
50
|
+
# We calculate state separately in here since our state functions aren't useful
|
|
51
|
+
for database_operation in self.database_operations:
|
|
52
|
+
to_state = from_state.clone()
|
|
53
|
+
database_operation.state_forwards(package_label, to_state)
|
|
54
|
+
database_operation.database_forwards(
|
|
55
|
+
package_label, schema_editor, from_state, to_state
|
|
56
|
+
)
|
|
57
|
+
from_state = to_state
|
|
58
|
+
|
|
59
|
+
def describe(self) -> str:
|
|
60
|
+
return "Custom state/database change combination"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RunSQL(Operation):
|
|
64
|
+
"""
|
|
65
|
+
Run some raw SQL.
|
|
66
|
+
|
|
67
|
+
Also accept a list of operations that represent the state change effected
|
|
68
|
+
by this SQL change, in case it's custom column/table creation/deletion.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
sql: str
|
|
74
|
+
| list[str | tuple[str, list[Any]]]
|
|
75
|
+
| tuple[str | tuple[str, list[Any]], ...],
|
|
76
|
+
*,
|
|
77
|
+
state_operations: list[Operation] | None = None,
|
|
78
|
+
elidable: bool = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.sql = sql
|
|
81
|
+
self.state_operations = state_operations or []
|
|
82
|
+
self.elidable = elidable
|
|
83
|
+
|
|
84
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
|
85
|
+
kwargs: dict[str, Any] = {
|
|
86
|
+
"sql": self.sql,
|
|
87
|
+
}
|
|
88
|
+
if self.state_operations:
|
|
89
|
+
kwargs["state_operations"] = self.state_operations
|
|
90
|
+
return (self.__class__.__qualname__, (), kwargs)
|
|
91
|
+
|
|
92
|
+
def state_forwards(self, package_label: str, state: ProjectState) -> None:
|
|
93
|
+
for state_operation in self.state_operations:
|
|
94
|
+
state_operation.state_forwards(package_label, state)
|
|
95
|
+
|
|
96
|
+
def database_forwards(
|
|
97
|
+
self,
|
|
98
|
+
package_label: str,
|
|
99
|
+
schema_editor: DatabaseSchemaEditor,
|
|
100
|
+
from_state: ProjectState,
|
|
101
|
+
to_state: ProjectState,
|
|
102
|
+
) -> None:
|
|
103
|
+
self._run_sql(schema_editor, self.sql)
|
|
104
|
+
|
|
105
|
+
def describe(self) -> str:
|
|
106
|
+
return "Raw SQL operation"
|
|
107
|
+
|
|
108
|
+
def _run_sql(
|
|
109
|
+
self,
|
|
110
|
+
schema_editor: DatabaseSchemaEditor,
|
|
111
|
+
sqls: str
|
|
112
|
+
| list[str | tuple[str, list[Any]]]
|
|
113
|
+
| tuple[str | tuple[str, list[Any]], ...],
|
|
114
|
+
) -> None:
|
|
115
|
+
if isinstance(sqls, list | tuple):
|
|
116
|
+
for sql_item in sqls:
|
|
117
|
+
params: list[Any] | None = None
|
|
118
|
+
sql: str
|
|
119
|
+
if isinstance(sql_item, list | tuple):
|
|
120
|
+
elements = len(sql_item)
|
|
121
|
+
if elements == 2:
|
|
122
|
+
sql, params = sql_item
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError("Expected a 2-tuple but got %d" % elements) # noqa: UP031
|
|
125
|
+
else:
|
|
126
|
+
sql = sql_item
|
|
127
|
+
schema_editor.execute(sql, params=params)
|
|
128
|
+
else:
|
|
129
|
+
# PostgreSQL can handle multi-statement scripts in a single execute call
|
|
130
|
+
schema_editor.execute(sqls, params=None)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RunPython(Operation):
|
|
134
|
+
"""
|
|
135
|
+
Run Python code in a context suitable for doing versioned ORM operations.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
reduces_to_sql = False
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
code: Callable[..., Any],
|
|
143
|
+
*,
|
|
144
|
+
atomic: bool | None = None,
|
|
145
|
+
elidable: bool = False,
|
|
146
|
+
) -> None:
|
|
147
|
+
self.atomic = atomic
|
|
148
|
+
# Forwards code
|
|
149
|
+
if not callable(code):
|
|
150
|
+
raise ValueError("RunPython must be supplied with a callable")
|
|
151
|
+
self.code = code
|
|
152
|
+
self.elidable = elidable
|
|
153
|
+
|
|
154
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
|
155
|
+
kwargs: dict[str, Any] = {
|
|
156
|
+
"code": self.code,
|
|
157
|
+
}
|
|
158
|
+
if self.atomic is not None:
|
|
159
|
+
kwargs["atomic"] = self.atomic
|
|
160
|
+
return (self.__class__.__qualname__, (), kwargs)
|
|
161
|
+
|
|
162
|
+
def state_forwards(self, package_label: str, state: Any) -> None:
|
|
163
|
+
# RunPython objects have no state effect. To add some, combine this
|
|
164
|
+
# with SeparateDatabaseAndState.
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def database_forwards(
|
|
168
|
+
self,
|
|
169
|
+
package_label: str,
|
|
170
|
+
schema_editor: DatabaseSchemaEditor,
|
|
171
|
+
from_state: ProjectState,
|
|
172
|
+
to_state: ProjectState,
|
|
173
|
+
) -> None:
|
|
174
|
+
# RunPython has access to all models. Ensure that all models are
|
|
175
|
+
# reloaded in case any are delayed.
|
|
176
|
+
from_state.clear_delayed_models_cache()
|
|
177
|
+
# We now execute the Python code in a context that contains a 'models'
|
|
178
|
+
# object, representing the versioned models as an app registry.
|
|
179
|
+
# We could try to override the global cache, but then people will still
|
|
180
|
+
# use direct imports, so we go with a documentation approach instead.
|
|
181
|
+
self.code(from_state.models_registry, schema_editor)
|
|
182
|
+
|
|
183
|
+
def describe(self) -> str:
|
|
184
|
+
return "Raw Python operation"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MigrationOptimizer:
|
|
7
|
+
"""
|
|
8
|
+
Power the optimization process, where you provide a list of Operations
|
|
9
|
+
and you are returned a list of equal or shorter length - operations
|
|
10
|
+
are merged into one if possible.
|
|
11
|
+
|
|
12
|
+
For example, a CreateModel and an AddField can be optimized into a
|
|
13
|
+
new CreateModel, and CreateModel and DeleteModel can be optimized into
|
|
14
|
+
nothing.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def optimize(self, operations: list[Any], package_label: str) -> list[Any]:
|
|
18
|
+
"""
|
|
19
|
+
Main optimization entry point. Pass in a list of Operation instances,
|
|
20
|
+
get out a new list of Operation instances.
|
|
21
|
+
|
|
22
|
+
Unfortunately, due to the scope of the optimization (two combinable
|
|
23
|
+
operations might be separated by several hundred others), this can't be
|
|
24
|
+
done as a peephole optimization with checks/output implemented on
|
|
25
|
+
the Operations themselves; instead, the optimizer looks at each
|
|
26
|
+
individual operation and scans forwards in the list to see if there
|
|
27
|
+
are any matches, stopping at boundaries - operations which can't
|
|
28
|
+
be optimized over (RunSQL, operations on the same field/model, etc.)
|
|
29
|
+
|
|
30
|
+
The inner loop is run until the starting list is the same as the result
|
|
31
|
+
list, and then the result is returned. This means that operation
|
|
32
|
+
optimization must be stable and always return an equal or shorter list.
|
|
33
|
+
"""
|
|
34
|
+
# Internal tracking variable for test assertions about # of loops
|
|
35
|
+
if package_label is None:
|
|
36
|
+
raise TypeError("package_label must be a str.")
|
|
37
|
+
while True:
|
|
38
|
+
result = self.optimize_inner(operations, package_label)
|
|
39
|
+
if result == operations:
|
|
40
|
+
return result
|
|
41
|
+
operations = result
|
|
42
|
+
|
|
43
|
+
def optimize_inner(self, operations: list[Any], package_label: str) -> list[Any]:
|
|
44
|
+
"""Inner optimization loop."""
|
|
45
|
+
new_operations = []
|
|
46
|
+
for i, operation in enumerate(operations):
|
|
47
|
+
right = True # Should we reduce on the right or on the left.
|
|
48
|
+
# Compare it to each operation after it
|
|
49
|
+
for j, other in enumerate(operations[i + 1 :]):
|
|
50
|
+
result = operation.reduce(other, package_label)
|
|
51
|
+
if isinstance(result, list):
|
|
52
|
+
in_between = operations[i + 1 : i + j + 1]
|
|
53
|
+
if right:
|
|
54
|
+
new_operations.extend(in_between)
|
|
55
|
+
new_operations.extend(result)
|
|
56
|
+
elif all(
|
|
57
|
+
op.reduce(other, package_label) is True for op in in_between
|
|
58
|
+
):
|
|
59
|
+
# Perform a left reduction if all of the in-between
|
|
60
|
+
# operations can optimize through other.
|
|
61
|
+
new_operations.extend(result)
|
|
62
|
+
new_operations.extend(in_between)
|
|
63
|
+
else:
|
|
64
|
+
# Otherwise keep trying.
|
|
65
|
+
new_operations.append(operation)
|
|
66
|
+
break
|
|
67
|
+
new_operations.extend(operations[i + j + 2 :])
|
|
68
|
+
return new_operations
|
|
69
|
+
elif not result:
|
|
70
|
+
# Can't perform a right reduction.
|
|
71
|
+
right = False
|
|
72
|
+
else:
|
|
73
|
+
new_operations.append(operation)
|
|
74
|
+
return new_operations
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import importlib
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from plain.packages import packages_registry
|
|
13
|
+
from plain.postgres.fields import NOT_PROVIDED
|
|
14
|
+
from plain.utils import timezone
|
|
15
|
+
|
|
16
|
+
from .loader import MigrationLoader
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from plain.postgres.fields import Field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MigrationQuestioner:
|
|
23
|
+
"""
|
|
24
|
+
Give the autodetector responses to questions it might have.
|
|
25
|
+
This base class has a built-in noninteractive mode, but the
|
|
26
|
+
interactive subclass is what the command-line arguments will use.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
defaults: dict[str, Any] | None = None,
|
|
32
|
+
specified_packages: set[str] | None = None,
|
|
33
|
+
dry_run: bool | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.defaults = defaults or {}
|
|
36
|
+
self.specified_packages = specified_packages or set()
|
|
37
|
+
self.dry_run = dry_run
|
|
38
|
+
|
|
39
|
+
def ask_initial(self, package_label: str) -> bool:
|
|
40
|
+
"""Should we create an initial migration for the app?"""
|
|
41
|
+
# If it was specified on the command line, definitely true
|
|
42
|
+
if package_label in self.specified_packages:
|
|
43
|
+
return True
|
|
44
|
+
# Otherwise, we look to see if it has a migrations module
|
|
45
|
+
# without any Python files in it, apart from __init__.py.
|
|
46
|
+
# Packages from the new app template will have these; the Python
|
|
47
|
+
# file check will ensure we skip South ones.
|
|
48
|
+
try:
|
|
49
|
+
package_config = packages_registry.get_package_config(package_label)
|
|
50
|
+
except LookupError: # It's a fake app.
|
|
51
|
+
return self.defaults.get("ask_initial", False)
|
|
52
|
+
migrations_import_path, _ = MigrationLoader.migrations_module(
|
|
53
|
+
package_config.package_label
|
|
54
|
+
)
|
|
55
|
+
if migrations_import_path is None:
|
|
56
|
+
# It's an application with migrations disabled.
|
|
57
|
+
return self.defaults.get("ask_initial", False)
|
|
58
|
+
try:
|
|
59
|
+
migrations_module = importlib.import_module(migrations_import_path)
|
|
60
|
+
except ImportError:
|
|
61
|
+
return self.defaults.get("ask_initial", False)
|
|
62
|
+
else:
|
|
63
|
+
if file := getattr(migrations_module, "__file__", None):
|
|
64
|
+
filenames = os.listdir(os.path.dirname(file))
|
|
65
|
+
elif hasattr(migrations_module, "__path__"):
|
|
66
|
+
if len(migrations_module.__path__) > 1:
|
|
67
|
+
return False
|
|
68
|
+
filenames = os.listdir(list(migrations_module.__path__)[0])
|
|
69
|
+
return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
|
|
70
|
+
|
|
71
|
+
def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
|
|
72
|
+
"""Adding a NOT NULL field to a model."""
|
|
73
|
+
# None means quit
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
|
|
77
|
+
"""Changing a NULL field to NOT NULL."""
|
|
78
|
+
# None means quit
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def ask_rename(
|
|
82
|
+
self, model_name: str, old_name: str, new_name: str, field_instance: Field
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""Was this field really renamed?"""
|
|
85
|
+
return self.defaults.get("ask_rename", False)
|
|
86
|
+
|
|
87
|
+
def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
|
|
88
|
+
"""Was this model really renamed?"""
|
|
89
|
+
return self.defaults.get("ask_rename_model", False)
|
|
90
|
+
|
|
91
|
+
def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
|
|
92
|
+
"""Adding an auto_now_add field to a model."""
|
|
93
|
+
# None means quit
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def ask_unique_callable_default_addition(
|
|
97
|
+
self, field_name: str, model_name: str
|
|
98
|
+
) -> Any:
|
|
99
|
+
"""Adding a unique field with a callable default."""
|
|
100
|
+
# None means continue.
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class InteractiveMigrationQuestioner(MigrationQuestioner):
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
defaults: dict[str, Any] | None = None,
|
|
108
|
+
specified_packages: set[str] | None = None,
|
|
109
|
+
dry_run: bool | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
super().__init__(
|
|
112
|
+
defaults=defaults, specified_packages=specified_packages, dry_run=dry_run
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _boolean_input(self, question: str, default: bool | None = None) -> bool:
|
|
116
|
+
return click.confirm(question, default=default)
|
|
117
|
+
|
|
118
|
+
def _choice_input(self, question: str, choices: list[str]) -> int:
|
|
119
|
+
choice_map = {str(i + 1): choice for i, choice in enumerate(choices)}
|
|
120
|
+
choice_map_str = "\n".join(
|
|
121
|
+
[f"{i}) {choice}" for i, choice in choice_map.items()]
|
|
122
|
+
)
|
|
123
|
+
choice = click.prompt(
|
|
124
|
+
f"{question}\n{choice_map_str}\nSelect an option",
|
|
125
|
+
type=click.Choice(choice_map.keys()),
|
|
126
|
+
)
|
|
127
|
+
return int(choice)
|
|
128
|
+
|
|
129
|
+
def _ask_default(self, default: str = "") -> Any:
|
|
130
|
+
"""
|
|
131
|
+
Prompt for a default value.
|
|
132
|
+
|
|
133
|
+
The ``default`` argument allows providing a custom default value (as a
|
|
134
|
+
string) which will be shown to the user and used as the return value
|
|
135
|
+
if the user doesn't provide any other input.
|
|
136
|
+
"""
|
|
137
|
+
click.echo("Please enter the default value as valid Python.")
|
|
138
|
+
if default:
|
|
139
|
+
click.echo(
|
|
140
|
+
f"Accept the default '{default}' by pressing 'Enter' or "
|
|
141
|
+
f"provide another value."
|
|
142
|
+
)
|
|
143
|
+
click.echo(
|
|
144
|
+
"The datetime and plain.utils.timezone modules are available, so "
|
|
145
|
+
"it is possible to provide e.g. timezone.now as a value."
|
|
146
|
+
)
|
|
147
|
+
click.echo("Type 'exit' to exit this prompt")
|
|
148
|
+
while True:
|
|
149
|
+
if default:
|
|
150
|
+
prompt = f"[default: {default}] >>> "
|
|
151
|
+
else:
|
|
152
|
+
prompt = ">>> "
|
|
153
|
+
code = click.prompt(prompt, default=default, show_default=False)
|
|
154
|
+
if not code and default:
|
|
155
|
+
code = default
|
|
156
|
+
if not code:
|
|
157
|
+
click.echo(
|
|
158
|
+
"Please enter some code, or 'exit' (without quotes) to exit."
|
|
159
|
+
)
|
|
160
|
+
elif code == "exit":
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
else:
|
|
163
|
+
try:
|
|
164
|
+
return eval(code, {}, {"datetime": datetime, "timezone": timezone})
|
|
165
|
+
except (SyntaxError, NameError) as e:
|
|
166
|
+
click.echo(f"Invalid input: {e}")
|
|
167
|
+
|
|
168
|
+
def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
|
|
169
|
+
"""Adding a NOT NULL field to a model."""
|
|
170
|
+
if not self.dry_run:
|
|
171
|
+
choice = self._choice_input(
|
|
172
|
+
f"It is impossible to add a non-nullable field '{field_name}' "
|
|
173
|
+
f"to {model_name} without specifying a default. This is "
|
|
174
|
+
f"because the database needs something to populate existing "
|
|
175
|
+
f"rows.\n"
|
|
176
|
+
f"Please select a fix:",
|
|
177
|
+
[
|
|
178
|
+
(
|
|
179
|
+
"Provide a one-off default now (will be set on all existing "
|
|
180
|
+
"rows with a null value for this column)"
|
|
181
|
+
),
|
|
182
|
+
"Quit and manually define a default value in models.py.",
|
|
183
|
+
],
|
|
184
|
+
)
|
|
185
|
+
if choice == 2:
|
|
186
|
+
sys.exit(3)
|
|
187
|
+
else:
|
|
188
|
+
return self._ask_default()
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
|
|
192
|
+
"""Changing a NULL field to NOT NULL."""
|
|
193
|
+
if not self.dry_run:
|
|
194
|
+
choice = self._choice_input(
|
|
195
|
+
f"It is impossible to change a nullable field '{field_name}' "
|
|
196
|
+
f"on {model_name} to non-nullable without providing a "
|
|
197
|
+
f"default. This is because the database needs something to "
|
|
198
|
+
f"populate existing rows.\n"
|
|
199
|
+
f"Please select a fix:",
|
|
200
|
+
[
|
|
201
|
+
(
|
|
202
|
+
"Provide a one-off default now (will be set on all existing "
|
|
203
|
+
"rows with a null value for this column)"
|
|
204
|
+
),
|
|
205
|
+
"Ignore for now. Existing rows that contain NULL values "
|
|
206
|
+
"will have to be handled manually, for example with a "
|
|
207
|
+
"RunPython or RunSQL operation.",
|
|
208
|
+
"Quit and manually define a default value in models.py.",
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
if choice == 2:
|
|
212
|
+
return NOT_PROVIDED
|
|
213
|
+
elif choice == 3:
|
|
214
|
+
sys.exit(3)
|
|
215
|
+
else:
|
|
216
|
+
return self._ask_default()
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def ask_rename(
|
|
220
|
+
self, model_name: str, old_name: str, new_name: str, field_instance: Field
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""Was this field really renamed?"""
|
|
223
|
+
msg = "Was %s.%s renamed to %s.%s (a %s)?"
|
|
224
|
+
return self._boolean_input(
|
|
225
|
+
msg
|
|
226
|
+
% (
|
|
227
|
+
model_name,
|
|
228
|
+
old_name,
|
|
229
|
+
model_name,
|
|
230
|
+
new_name,
|
|
231
|
+
field_instance.__class__.__name__,
|
|
232
|
+
),
|
|
233
|
+
default=False,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
|
|
237
|
+
"""Was this model really renamed?"""
|
|
238
|
+
msg = "Was the model %s.%s renamed to %s?"
|
|
239
|
+
return self._boolean_input(
|
|
240
|
+
msg
|
|
241
|
+
% (
|
|
242
|
+
old_model_state.package_label,
|
|
243
|
+
old_model_state.name,
|
|
244
|
+
new_model_state.name,
|
|
245
|
+
),
|
|
246
|
+
default=False,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
|
|
250
|
+
"""Adding an auto_now_add field to a model."""
|
|
251
|
+
if not self.dry_run:
|
|
252
|
+
choice = self._choice_input(
|
|
253
|
+
f"It is impossible to add the field '{field_name}' with "
|
|
254
|
+
f"'auto_now_add=True' to {model_name} without providing a "
|
|
255
|
+
f"default. This is because the database needs something to "
|
|
256
|
+
f"populate existing rows.\n",
|
|
257
|
+
[
|
|
258
|
+
"Provide a one-off default now which will be set on all "
|
|
259
|
+
"existing rows",
|
|
260
|
+
"Quit and manually define a default value in models.py.",
|
|
261
|
+
],
|
|
262
|
+
)
|
|
263
|
+
if choice == 2:
|
|
264
|
+
sys.exit(3)
|
|
265
|
+
else:
|
|
266
|
+
return self._ask_default(default="timezone.now")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def ask_unique_callable_default_addition(
|
|
270
|
+
self, field_name: str, model_name: str
|
|
271
|
+
) -> Any:
|
|
272
|
+
"""Adding a unique field with a callable default."""
|
|
273
|
+
if not self.dry_run:
|
|
274
|
+
choice = self._choice_input(
|
|
275
|
+
f"Callable default on unique field {model_name}.{field_name} "
|
|
276
|
+
f"will not generate unique values upon migrating.\n"
|
|
277
|
+
f"Please choose how to proceed:\n",
|
|
278
|
+
[
|
|
279
|
+
"Continue making this migration as the first step in "
|
|
280
|
+
"writing a manual migration to generate unique values.",
|
|
281
|
+
"Quit and edit field options in models.py.",
|
|
282
|
+
],
|
|
283
|
+
)
|
|
284
|
+
if choice == 2:
|
|
285
|
+
sys.exit(3)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class NonInteractiveMigrationQuestioner(MigrationQuestioner):
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
defaults: dict[str, Any] | None = None,
|
|
293
|
+
specified_packages: set[str] | None = None,
|
|
294
|
+
dry_run: bool | None = None,
|
|
295
|
+
verbosity: int = 1,
|
|
296
|
+
log: Callable[[str], Any] | None = None,
|
|
297
|
+
) -> None:
|
|
298
|
+
self.verbosity = verbosity
|
|
299
|
+
self.log = log
|
|
300
|
+
super().__init__(
|
|
301
|
+
defaults=defaults,
|
|
302
|
+
specified_packages=specified_packages,
|
|
303
|
+
dry_run=dry_run,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def log_lack_of_migration(
|
|
307
|
+
self, field_name: str, model_name: str, reason: str
|
|
308
|
+
) -> None:
|
|
309
|
+
if self.verbosity > 0 and self.log:
|
|
310
|
+
self.log(
|
|
311
|
+
f"Field '{field_name}' on model '{model_name}' not migrated: {reason}."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
|
|
315
|
+
# We can't ask the user, so act like the user aborted.
|
|
316
|
+
self.log_lack_of_migration(
|
|
317
|
+
field_name,
|
|
318
|
+
model_name,
|
|
319
|
+
"it is impossible to add a non-nullable field without specifying a default",
|
|
320
|
+
)
|
|
321
|
+
sys.exit(3)
|
|
322
|
+
|
|
323
|
+
def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
|
|
324
|
+
# We can't ask the user, so set as not provided.
|
|
325
|
+
if self.log:
|
|
326
|
+
self.log(
|
|
327
|
+
f"Field '{field_name}' on model '{model_name}' given a default of "
|
|
328
|
+
f"NOT PROVIDED and must be corrected."
|
|
329
|
+
)
|
|
330
|
+
return NOT_PROVIDED
|
|
331
|
+
|
|
332
|
+
def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
|
|
333
|
+
# We can't ask the user, so act like the user aborted.
|
|
334
|
+
self.log_lack_of_migration(
|
|
335
|
+
field_name,
|
|
336
|
+
model_name,
|
|
337
|
+
"it is impossible to add a field with 'auto_now_add=True' without "
|
|
338
|
+
"specifying a default",
|
|
339
|
+
)
|
|
340
|
+
sys.exit(3)
|