pum 1.2.3__py3-none-any.whl → 1.3.1__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.
- pum/__init__.py +71 -10
- pum/changelog.py +61 -1
- pum/checker.py +444 -214
- pum/cli.py +279 -135
- pum/config_model.py +56 -33
- pum/connection.py +30 -0
- pum/dependency_handler.py +69 -4
- pum/dumper.py +14 -4
- pum/exceptions.py +9 -0
- pum/feedback.py +119 -0
- pum/hook.py +95 -29
- pum/info.py +0 -2
- pum/parameter.py +4 -0
- pum/pum_config.py +103 -20
- pum/report_generator.py +1043 -0
- pum/role_manager.py +151 -23
- pum/schema_migrations.py +163 -30
- pum/sql_content.py +83 -21
- pum/upgrader.py +287 -23
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/METADATA +6 -2
- pum-1.3.1.dist-info/RECORD +25 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/WHEEL +1 -1
- pum-1.2.3.dist-info/RECORD +0 -22
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/entry_points.txt +0 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/top_level.txt +0 -0
pum/role_manager.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import enum
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
3
|
import copy
|
|
4
4
|
import psycopg
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
7
|
from .sql_content import SqlContent
|
|
8
|
+
from .exceptions import PumException
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .feedback import Feedback
|
|
8
12
|
|
|
9
13
|
logger = logging.getLogger(__name__)
|
|
10
14
|
|
|
@@ -40,12 +44,14 @@ class Permission:
|
|
|
40
44
|
role: str,
|
|
41
45
|
connection: psycopg.Connection,
|
|
42
46
|
commit: bool = False,
|
|
47
|
+
feedback: Optional["Feedback"] = None,
|
|
43
48
|
) -> None:
|
|
44
49
|
"""Grant the permission to the specified role.
|
|
45
50
|
Args:
|
|
46
51
|
role: The name of the role to grant the permission to.
|
|
47
52
|
connection: The database connection to execute the SQL statements.
|
|
48
53
|
commit: Whether to commit the transaction. Defaults to False.
|
|
54
|
+
feedback: Optional feedback object for progress reporting.
|
|
49
55
|
"""
|
|
50
56
|
if not isinstance(role, str):
|
|
51
57
|
raise TypeError("Role must be a string.")
|
|
@@ -54,13 +60,37 @@ class Permission:
|
|
|
54
60
|
raise ValueError("Schemas must be defined for the permission.")
|
|
55
61
|
|
|
56
62
|
for schema in self.schemas:
|
|
57
|
-
|
|
63
|
+
if feedback and feedback.is_cancelled():
|
|
64
|
+
raise PumException("Permission grant cancelled by user")
|
|
65
|
+
|
|
66
|
+
# Detect if schema exists; if not, warn and continue
|
|
67
|
+
cursor = SqlContent("SELECT 1 FROM pg_namespace WHERE nspname = {schema}").execute(
|
|
68
|
+
connection=connection,
|
|
69
|
+
commit=False,
|
|
70
|
+
parameters={"schema": psycopg.sql.Literal(schema)},
|
|
71
|
+
)
|
|
72
|
+
if not cursor._pum_results or not cursor._pum_results[0]:
|
|
73
|
+
logger.warning(
|
|
74
|
+
f"Schema {schema} does not exist; skipping grant of {self.type.value} "
|
|
75
|
+
f"permission to role {role}."
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
logger.debug(
|
|
80
|
+
f"Granting {self.type.value} permission on schema {schema} to role {role}."
|
|
81
|
+
)
|
|
58
82
|
if self.type == PermissionType.READ:
|
|
59
83
|
SqlContent("""
|
|
60
84
|
GRANT USAGE ON SCHEMA {schema} TO {role};
|
|
61
|
-
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA {schema} TO {role};
|
|
62
85
|
GRANT SELECT, REFERENCES, TRIGGER ON ALL TABLES IN SCHEMA {schema} TO {role};
|
|
86
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA {schema} TO {role};
|
|
87
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA {schema} TO {role};
|
|
88
|
+
GRANT EXECUTE ON ALL ROUTINES IN SCHEMA {schema} TO {role};
|
|
63
89
|
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT SELECT, REFERENCES, TRIGGER ON TABLES TO {role};
|
|
90
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT SELECT ON SEQUENCES TO {role};
|
|
91
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT EXECUTE ON FUNCTIONS TO {role};
|
|
92
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT EXECUTE ON ROUTINES TO {role};
|
|
93
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT USAGE ON TYPES TO {role};
|
|
64
94
|
""").execute(
|
|
65
95
|
connection=connection,
|
|
66
96
|
commit=False,
|
|
@@ -69,12 +99,20 @@ class Permission:
|
|
|
69
99
|
"role": psycopg.sql.Identifier(role),
|
|
70
100
|
},
|
|
71
101
|
)
|
|
102
|
+
# Grant permissions on existing types
|
|
103
|
+
self._grant_existing_types(connection, schema, role, "USAGE")
|
|
72
104
|
elif self.type == PermissionType.WRITE:
|
|
73
105
|
SqlContent("""
|
|
74
106
|
GRANT ALL ON SCHEMA {schema} TO {role};
|
|
75
107
|
GRANT ALL ON ALL TABLES IN SCHEMA {schema} TO {role};
|
|
76
108
|
GRANT ALL ON ALL SEQUENCES IN SCHEMA {schema} TO {role};
|
|
109
|
+
GRANT ALL ON ALL FUNCTIONS IN SCHEMA {schema} TO {role};
|
|
110
|
+
GRANT ALL ON ALL ROUTINES IN SCHEMA {schema} TO {role};
|
|
77
111
|
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON TABLES TO {role};
|
|
112
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON SEQUENCES TO {role};
|
|
113
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON FUNCTIONS TO {role};
|
|
114
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON ROUTINES TO {role};
|
|
115
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON TYPES TO {role};
|
|
78
116
|
""").execute(
|
|
79
117
|
connection=connection,
|
|
80
118
|
commit=False,
|
|
@@ -83,15 +121,62 @@ class Permission:
|
|
|
83
121
|
"role": psycopg.sql.Identifier(role),
|
|
84
122
|
},
|
|
85
123
|
)
|
|
124
|
+
# Grant permissions on existing types
|
|
125
|
+
self._grant_existing_types(connection, schema, role, "ALL")
|
|
86
126
|
else:
|
|
87
127
|
raise ValueError(f"Unknown permission type: {self.type}")
|
|
88
128
|
|
|
89
129
|
if commit:
|
|
130
|
+
if feedback:
|
|
131
|
+
feedback.lock_cancellation()
|
|
90
132
|
connection.commit()
|
|
91
133
|
|
|
134
|
+
def _grant_existing_types(
|
|
135
|
+
self, connection: psycopg.Connection, schema: str, role: str, privilege: str
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Grant permissions on all existing types in a schema.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
connection: The database connection.
|
|
141
|
+
schema: The schema name.
|
|
142
|
+
role: The role name.
|
|
143
|
+
privilege: The privilege to grant (USAGE or ALL).
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
# Query for all types in the schema (excluding array types and internal types)
|
|
147
|
+
cursor = connection.cursor()
|
|
148
|
+
cursor.execute(
|
|
149
|
+
"""
|
|
150
|
+
SELECT t.typname
|
|
151
|
+
FROM pg_type t
|
|
152
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
153
|
+
WHERE n.nspname = %s
|
|
154
|
+
AND t.typtype IN ('e', 'c', 'd', 'b', 'r') -- enum, composite, domain, base, range
|
|
155
|
+
AND t.typname NOT LIKE '_%%' -- exclude array types
|
|
156
|
+
""",
|
|
157
|
+
(schema,),
|
|
158
|
+
)
|
|
159
|
+
types = cursor.fetchall()
|
|
160
|
+
|
|
161
|
+
# Grant permissions on each type
|
|
162
|
+
for (type_name,) in types:
|
|
163
|
+
grant_sql = psycopg.sql.SQL(
|
|
164
|
+
"GRANT {privilege} ON TYPE {schema}.{type_name} TO {role}"
|
|
165
|
+
).format(
|
|
166
|
+
privilege=psycopg.sql.SQL(privilege),
|
|
167
|
+
schema=psycopg.sql.Identifier(schema),
|
|
168
|
+
type_name=psycopg.sql.Identifier(type_name),
|
|
169
|
+
role=psycopg.sql.Identifier(role),
|
|
170
|
+
)
|
|
171
|
+
cursor.execute(grant_sql)
|
|
172
|
+
|
|
173
|
+
def __repr__(self) -> str:
|
|
174
|
+
"""Return a string representation of the Permission object."""
|
|
175
|
+
return f"<Permission: {self.type.value} on {self.schemas}>"
|
|
176
|
+
|
|
92
177
|
|
|
93
178
|
class Role:
|
|
94
|
-
"""
|
|
179
|
+
"""
|
|
95
180
|
Represents a database role with associated permissions and optional inheritance.
|
|
96
181
|
The Role class encapsulates the concept of a database role, including its name,
|
|
97
182
|
permissions, optional inheritance from another role, and an optional description.
|
|
@@ -138,30 +223,36 @@ class Role:
|
|
|
138
223
|
Returns:
|
|
139
224
|
bool: True if the role exists, False otherwise.
|
|
140
225
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
commit=False,
|
|
146
|
-
parameters={"name": psycopg.sql.Literal(self.name)},
|
|
147
|
-
)
|
|
148
|
-
.fetchone()
|
|
149
|
-
is not None
|
|
226
|
+
cursor = SqlContent("SELECT 1 FROM pg_roles WHERE rolname = {name}").execute(
|
|
227
|
+
connection=connection,
|
|
228
|
+
commit=False,
|
|
229
|
+
parameters={"name": psycopg.sql.Literal(self.name)},
|
|
150
230
|
)
|
|
231
|
+
return bool(cursor._pum_results)
|
|
151
232
|
|
|
152
233
|
def create(
|
|
153
|
-
self,
|
|
234
|
+
self,
|
|
235
|
+
connection: psycopg.Connection,
|
|
236
|
+
grant: bool = False,
|
|
237
|
+
commit: bool = False,
|
|
238
|
+
feedback: Optional["Feedback"] = None,
|
|
154
239
|
) -> None:
|
|
155
240
|
"""Create the role in the database.
|
|
156
241
|
Args:
|
|
157
242
|
connection: The database connection to execute the SQL statements.
|
|
158
243
|
grant: Whether to grant permissions to the role. Defaults to False.
|
|
159
244
|
commit: Whether to commit the transaction. Defaults to False.
|
|
245
|
+
feedback: Optional feedback object for progress reporting.
|
|
160
246
|
"""
|
|
247
|
+
if feedback and feedback.is_cancelled():
|
|
248
|
+
from .exceptions import PumException
|
|
249
|
+
|
|
250
|
+
raise PumException("Role creation cancelled by user")
|
|
251
|
+
|
|
161
252
|
if self.exists(connection):
|
|
162
|
-
logger.
|
|
253
|
+
logger.debug(f"Role {self.name} already exists, skipping creation.")
|
|
163
254
|
else:
|
|
164
|
-
logger.
|
|
255
|
+
logger.debug(f"Creating role {self.name}.")
|
|
165
256
|
SqlContent(
|
|
166
257
|
"CREATE ROLE {name} NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION"
|
|
167
258
|
).execute(
|
|
@@ -189,9 +280,13 @@ class Role:
|
|
|
189
280
|
)
|
|
190
281
|
if grant:
|
|
191
282
|
for permission in self.permissions():
|
|
192
|
-
permission.grant(
|
|
283
|
+
permission.grant(
|
|
284
|
+
role=self.name, connection=connection, commit=False, feedback=feedback
|
|
285
|
+
)
|
|
193
286
|
|
|
194
287
|
if commit:
|
|
288
|
+
if feedback:
|
|
289
|
+
feedback.lock_cancellation()
|
|
195
290
|
connection.commit()
|
|
196
291
|
|
|
197
292
|
|
|
@@ -232,28 +327,61 @@ class RoleManager:
|
|
|
232
327
|
)
|
|
233
328
|
|
|
234
329
|
def create_roles(
|
|
235
|
-
self,
|
|
330
|
+
self,
|
|
331
|
+
connection: psycopg.Connection,
|
|
332
|
+
grant: bool = False,
|
|
333
|
+
commit: bool = False,
|
|
334
|
+
feedback: Optional["Feedback"] = None,
|
|
236
335
|
) -> None:
|
|
237
336
|
"""Create roles in the database.
|
|
238
337
|
Args:
|
|
239
338
|
connection: The database connection to execute the SQL statements.
|
|
240
339
|
grant: Whether to grant permissions to the roles. Defaults to False.
|
|
241
340
|
commit: Whether to commit the transaction. Defaults to False.
|
|
341
|
+
feedback: Optional feedback object for progress reporting.
|
|
242
342
|
"""
|
|
243
|
-
|
|
244
|
-
|
|
343
|
+
roles_list = list(self.roles.values())
|
|
344
|
+
for role in roles_list:
|
|
345
|
+
if feedback and feedback.is_cancelled():
|
|
346
|
+
from .exceptions import PumException
|
|
347
|
+
|
|
348
|
+
raise PumException("Role creation cancelled by user")
|
|
349
|
+
if feedback:
|
|
350
|
+
feedback.increment_step()
|
|
351
|
+
feedback.report_progress(f"Creating role: {role.name}")
|
|
352
|
+
role.create(connection=connection, commit=False, grant=grant, feedback=feedback)
|
|
245
353
|
if commit:
|
|
354
|
+
if feedback:
|
|
355
|
+
feedback.lock_cancellation()
|
|
246
356
|
connection.commit()
|
|
247
357
|
|
|
248
|
-
def grant_permissions(
|
|
358
|
+
def grant_permissions(
|
|
359
|
+
self,
|
|
360
|
+
connection: psycopg.Connection,
|
|
361
|
+
commit: bool = False,
|
|
362
|
+
feedback: Optional["Feedback"] = None,
|
|
363
|
+
) -> None:
|
|
249
364
|
"""Grant permissions to the roles in the database.
|
|
250
365
|
Args:
|
|
251
366
|
connection: The database connection to execute the SQL statements.
|
|
252
367
|
commit: Whether to commit the transaction. Defaults to False.
|
|
368
|
+
feedback: Optional feedback object for progress reporting.
|
|
253
369
|
"""
|
|
254
|
-
|
|
370
|
+
roles_list = list(self.roles.values())
|
|
371
|
+
for role in roles_list:
|
|
372
|
+
if feedback and feedback.is_cancelled():
|
|
373
|
+
from .exceptions import PumException
|
|
374
|
+
|
|
375
|
+
raise PumException("Permission grant cancelled by user")
|
|
376
|
+
if feedback:
|
|
377
|
+
feedback.increment_step()
|
|
378
|
+
feedback.report_progress(f"Granting permissions to role: {role.name}")
|
|
255
379
|
for permission in role.permissions():
|
|
256
|
-
permission.grant(
|
|
380
|
+
permission.grant(
|
|
381
|
+
role=role.name, connection=connection, commit=False, feedback=feedback
|
|
382
|
+
)
|
|
257
383
|
logger.info("All permissions granted to roles.")
|
|
258
384
|
if commit:
|
|
385
|
+
if feedback:
|
|
386
|
+
feedback.lock_cancellation()
|
|
259
387
|
connection.commit()
|
pum/schema_migrations.py
CHANGED
|
@@ -3,18 +3,28 @@ import logging
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
import packaging
|
|
6
|
+
import packaging.version
|
|
6
7
|
import psycopg
|
|
7
8
|
import psycopg.sql
|
|
8
9
|
|
|
9
|
-
from .
|
|
10
|
-
from .exceptions import PumException
|
|
10
|
+
from .exceptions import PumSchemaMigrationError, PumSchemaMigrationNoBaselineError
|
|
11
11
|
from .sql_content import SqlContent
|
|
12
|
+
from .pum_config import PumConfig
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
|
-
MIGRATION_TABLE_VERSION =
|
|
16
|
+
MIGRATION_TABLE_VERSION = 2 # Current schema version
|
|
16
17
|
MIGRATION_TABLE_NAME = "pum_migrations"
|
|
17
18
|
|
|
19
|
+
# TABLE VERSION HISTORY
|
|
20
|
+
#
|
|
21
|
+
# Version 1:
|
|
22
|
+
# Initial version with columns id, date_installed, module, version,
|
|
23
|
+
# beta_testing, changelog_files, parameters, migration_table_version
|
|
24
|
+
#
|
|
25
|
+
# Version 2:
|
|
26
|
+
# Changed migration_table_version type to integer and set module NOT NULL (version 2025.1 => 1, module 'tww')
|
|
27
|
+
|
|
18
28
|
|
|
19
29
|
class SchemaMigrations:
|
|
20
30
|
"""Manage the schema migrations in the database.
|
|
@@ -36,6 +46,9 @@ class SchemaMigrations:
|
|
|
36
46
|
psycopg.sql.Identifier(MIGRATION_TABLE_NAME),
|
|
37
47
|
]
|
|
38
48
|
)
|
|
49
|
+
self.migration_table_identifier_str = (
|
|
50
|
+
f"{self.config.config.pum.migration_table_schema}.{MIGRATION_TABLE_NAME}"
|
|
51
|
+
)
|
|
39
52
|
|
|
40
53
|
def exists(self, connection: psycopg.Connection) -> bool:
|
|
41
54
|
"""Check if the schema_migrations information table exists.
|
|
@@ -62,7 +75,8 @@ class SchemaMigrations:
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
65
|
-
|
|
78
|
+
result = cursor._pum_results[0] if cursor._pum_results else None
|
|
79
|
+
return result[0] if result else False
|
|
66
80
|
|
|
67
81
|
def exists_in_other_schemas(self, connection: psycopg.Connection) -> list[str]:
|
|
68
82
|
"""Check if the schema_migrations information table exists in other schemas.
|
|
@@ -86,7 +100,7 @@ class SchemaMigrations:
|
|
|
86
100
|
"schema": psycopg.sql.Literal(self.config.config.pum.migration_table_schema),
|
|
87
101
|
}
|
|
88
102
|
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
89
|
-
return [row[0] for row in cursor.
|
|
103
|
+
return [row[0] for row in (cursor._pum_results or [])]
|
|
90
104
|
|
|
91
105
|
def create(
|
|
92
106
|
self,
|
|
@@ -103,13 +117,13 @@ class SchemaMigrations:
|
|
|
103
117
|
distinct schemas. Default is false.
|
|
104
118
|
"""
|
|
105
119
|
if self.exists(connection):
|
|
106
|
-
logger.
|
|
120
|
+
logger.debug(
|
|
107
121
|
f"{self.config.config.pum.migration_table_schema}.pum_migrations table already exists."
|
|
108
122
|
)
|
|
109
123
|
return
|
|
110
124
|
|
|
111
125
|
if not allow_multiple_schemas and len(self.exists_in_other_schemas(connection)) > 0:
|
|
112
|
-
raise
|
|
126
|
+
raise PumSchemaMigrationError(
|
|
113
127
|
f"Another {self.config.config.pum.migration_table_schema}.{MIGRATION_TABLE_NAME} table exists in another schema (). "
|
|
114
128
|
"Please use the allow_multiple_schemas option to create a new one."
|
|
115
129
|
)
|
|
@@ -130,30 +144,81 @@ class SchemaMigrations:
|
|
|
130
144
|
(
|
|
131
145
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
132
146
|
date_installed timestamp without time zone NOT NULL DEFAULT now(),
|
|
133
|
-
module character varying(50)
|
|
147
|
+
module character varying(50) NOT NULL,
|
|
134
148
|
version character varying(50) NOT NULL,
|
|
135
149
|
beta_testing boolean NOT NULL DEFAULT false,
|
|
136
150
|
changelog_files text[],
|
|
137
151
|
parameters jsonb,
|
|
138
|
-
migration_table_version
|
|
152
|
+
migration_table_version integer NOT NULL DEFAULT {version}
|
|
139
153
|
);
|
|
140
154
|
"""
|
|
141
155
|
)
|
|
142
156
|
|
|
143
|
-
comment_query = psycopg.sql.SQL(
|
|
144
|
-
"COMMENT ON TABLE {table} IS 'version: 1 -- schema_migration table version';"
|
|
145
|
-
)
|
|
157
|
+
comment_query = psycopg.sql.SQL("COMMENT ON TABLE {table} IS 'migration_table_version: 2';")
|
|
146
158
|
|
|
147
159
|
if create_schema_query:
|
|
148
160
|
SqlContent(create_schema_query).execute(connection, parameters=parameters)
|
|
149
161
|
SqlContent(create_table_query).execute(connection, parameters=parameters)
|
|
150
162
|
SqlContent(comment_query).execute(connection, parameters=parameters)
|
|
151
163
|
|
|
152
|
-
logger.info(f"Created {
|
|
164
|
+
logger.info(f"Created migration table: {self.migration_table_identifier_str}")
|
|
153
165
|
|
|
154
166
|
if commit:
|
|
155
167
|
connection.commit()
|
|
156
168
|
|
|
169
|
+
def migration_table_version(self, connection: psycopg.Connection) -> int:
|
|
170
|
+
"""Return the migration table version.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
connection: The database connection to get the migration table version.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
int | None: The migration table version, or None if the table does not exist.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
query = psycopg.sql.SQL(
|
|
180
|
+
"""
|
|
181
|
+
SELECT migration_table_version
|
|
182
|
+
FROM {table}
|
|
183
|
+
ORDER BY migration_table_version DESC
|
|
184
|
+
LIMIT 1;
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
parameters = {
|
|
189
|
+
"table": self.migration_table_identifier,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
193
|
+
row = cursor._pum_results[0] if cursor._pum_results else None
|
|
194
|
+
if row is None:
|
|
195
|
+
raise PumSchemaMigrationError(
|
|
196
|
+
f"Migration table {self.migration_table_identifier_str} does not exist."
|
|
197
|
+
)
|
|
198
|
+
return row[0]
|
|
199
|
+
|
|
200
|
+
def update_migration_table_schema(self, connection: psycopg.Connection) -> None:
|
|
201
|
+
"""Update the migration table schema to the latest version.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
connection: The database connection to update the table.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
table_version = self.migration_table_version(connection)
|
|
208
|
+
logger.info(
|
|
209
|
+
f"Updating migration table {self.migration_table_identifier_str} from version {table_version} to {MIGRATION_TABLE_VERSION}."
|
|
210
|
+
)
|
|
211
|
+
if table_version == 1:
|
|
212
|
+
alter_query = psycopg.sql.SQL("""
|
|
213
|
+
ALTER TABLE {table} ALTER COLUMN migration_table_version ALTER TYPE integer SET DEFAULT {version} USING 1;
|
|
214
|
+
ALTER TABLE {table} ALTER COLUMN module SET NOT NULL USING 'tww';
|
|
215
|
+
""")
|
|
216
|
+
parameters = {
|
|
217
|
+
"table": self.migration_table_identifier,
|
|
218
|
+
"version": psycopg.sql.Literal(MIGRATION_TABLE_VERSION),
|
|
219
|
+
}
|
|
220
|
+
SqlContent(alter_query).execute(connection, parameters=parameters)
|
|
221
|
+
|
|
157
222
|
def set_baseline(
|
|
158
223
|
self,
|
|
159
224
|
connection: psycopg.Connection,
|
|
@@ -175,24 +240,37 @@ class SchemaMigrations:
|
|
|
175
240
|
commit: If true, the transaction is committed. The default is False.
|
|
176
241
|
|
|
177
242
|
"""
|
|
178
|
-
|
|
179
|
-
|
|
243
|
+
version_str = version
|
|
244
|
+
version_packaging = version
|
|
245
|
+
if isinstance(version_str, packaging.version.Version):
|
|
246
|
+
version_str = str(version_str)
|
|
247
|
+
if isinstance(version_packaging, str):
|
|
248
|
+
version_packaging = packaging.version.parse(version_packaging)
|
|
180
249
|
pattern = re.compile(r"^\d+\.\d+(\.\d+)?$")
|
|
181
|
-
if not re.match(pattern,
|
|
250
|
+
if not re.match(pattern, version_str):
|
|
182
251
|
raise ValueError(f"Wrong version format: {version}. Must be x.y or x.y.z")
|
|
183
252
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
253
|
+
try:
|
|
254
|
+
current = self.baseline(connection=connection)
|
|
255
|
+
except PumSchemaMigrationNoBaselineError:
|
|
256
|
+
current = None
|
|
257
|
+
if current:
|
|
258
|
+
self.update_migration_table_schema(connection)
|
|
259
|
+
if current and current >= version_packaging:
|
|
260
|
+
raise PumSchemaMigrationError(
|
|
261
|
+
f"Cannot set baseline {version_str} as it is already set at {current}."
|
|
262
|
+
)
|
|
187
263
|
|
|
188
264
|
code = psycopg.sql.SQL("""
|
|
189
265
|
INSERT INTO {table} (
|
|
266
|
+
module,
|
|
190
267
|
version,
|
|
191
268
|
beta_testing,
|
|
192
269
|
migration_table_version,
|
|
193
270
|
changelog_files,
|
|
194
271
|
parameters
|
|
195
272
|
) VALUES (
|
|
273
|
+
{module},
|
|
196
274
|
{version},
|
|
197
275
|
{beta_testing},
|
|
198
276
|
{migration_table_version},
|
|
@@ -202,7 +280,8 @@ INSERT INTO {table} (
|
|
|
202
280
|
|
|
203
281
|
query_parameters = {
|
|
204
282
|
"table": self.migration_table_identifier,
|
|
205
|
-
"
|
|
283
|
+
"module": psycopg.sql.Literal(self.config.config.pum.module),
|
|
284
|
+
"version": psycopg.sql.Literal(version_str),
|
|
206
285
|
"beta_testing": psycopg.sql.Literal(beta_testing),
|
|
207
286
|
"migration_table_version": psycopg.sql.Literal(MIGRATION_TABLE_VERSION),
|
|
208
287
|
"changelog_files": psycopg.sql.Literal(changelog_files or []),
|
|
@@ -210,11 +289,25 @@ INSERT INTO {table} (
|
|
|
210
289
|
}
|
|
211
290
|
|
|
212
291
|
logger.info(
|
|
213
|
-
f"Setting baseline version {version} in {self.
|
|
292
|
+
f"Setting baseline version {version} in {self.migration_table_identifier_str} table"
|
|
214
293
|
)
|
|
215
294
|
SqlContent(code).execute(connection, parameters=query_parameters, commit=commit)
|
|
216
295
|
|
|
217
|
-
def
|
|
296
|
+
def has_baseline(self, connection: psycopg.Connection) -> bool:
|
|
297
|
+
"""Check if the migration table has a baseline version.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
connection: The database connection to check for the baseline version.
|
|
301
|
+
Returns:
|
|
302
|
+
bool: True if the baseline version exists, False otherwise.
|
|
303
|
+
"""
|
|
304
|
+
try:
|
|
305
|
+
self.baseline(connection=connection)
|
|
306
|
+
return True
|
|
307
|
+
except PumSchemaMigrationError:
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def baseline(self, connection: psycopg.Connection) -> packaging.version.Version:
|
|
218
311
|
"""Return the baseline version from the migration table.
|
|
219
312
|
|
|
220
313
|
Args:
|
|
@@ -222,12 +315,17 @@ INSERT INTO {table} (
|
|
|
222
315
|
The database connection to get the baseline version.
|
|
223
316
|
|
|
224
317
|
Returns:
|
|
225
|
-
|
|
318
|
+
packaging.version.Version | None: The baseline version.
|
|
226
319
|
|
|
320
|
+
Raises:
|
|
321
|
+
PumSchemaMigrationError: If the migration table does not exist or if no baseline version is found
|
|
322
|
+
PumSchemaMigrationNoBaselineError: If the migration table does not exist
|
|
227
323
|
"""
|
|
228
324
|
|
|
229
325
|
if not self.exists(connection=connection):
|
|
230
|
-
|
|
326
|
+
raise PumSchemaMigrationError(
|
|
327
|
+
f"{self.migration_table_identifier_str} table does not exist."
|
|
328
|
+
)
|
|
231
329
|
|
|
232
330
|
query = psycopg.sql.SQL(
|
|
233
331
|
"""
|
|
@@ -247,10 +345,12 @@ INSERT INTO {table} (
|
|
|
247
345
|
}
|
|
248
346
|
|
|
249
347
|
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
250
|
-
row = cursor.
|
|
348
|
+
row = cursor._pum_results[0] if cursor._pum_results else None
|
|
251
349
|
if row is None:
|
|
252
|
-
|
|
253
|
-
|
|
350
|
+
raise PumSchemaMigrationNoBaselineError(
|
|
351
|
+
f"Baseline version not found in the {self.migration_table_identifier_str} table."
|
|
352
|
+
)
|
|
353
|
+
return packaging.version.parse(row[0])
|
|
254
354
|
|
|
255
355
|
def migration_details(self, connection: psycopg.Connection, version: str | None = None) -> dict:
|
|
256
356
|
"""Return the migration details from the migration table.
|
|
@@ -265,6 +365,8 @@ INSERT INTO {table} (
|
|
|
265
365
|
Returns:
|
|
266
366
|
dict: The migration details.
|
|
267
367
|
|
|
368
|
+
Raises:
|
|
369
|
+
PumSchemaMigrationError: If the migration table does not exist or if no migration details are found.
|
|
268
370
|
"""
|
|
269
371
|
query = None
|
|
270
372
|
if version is None:
|
|
@@ -300,7 +402,38 @@ INSERT INTO {table} (
|
|
|
300
402
|
}
|
|
301
403
|
|
|
302
404
|
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
303
|
-
row = cursor.
|
|
405
|
+
row = cursor._pum_results[0] if cursor._pum_results else None
|
|
304
406
|
if row is None:
|
|
305
|
-
|
|
306
|
-
|
|
407
|
+
raise PumSchemaMigrationError(
|
|
408
|
+
f"Migration details not found for version {version} in the {self.migration_table_identifier_str} table."
|
|
409
|
+
)
|
|
410
|
+
return dict(zip([desc[0] for desc in cursor._pum_description], row, strict=False))
|
|
411
|
+
|
|
412
|
+
def compare(self, connection: psycopg.Connection) -> int:
|
|
413
|
+
"""Compare the migrations details in the database to the changelogs in the source.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
connection: The database connection to get the baseline version.
|
|
417
|
+
Returns:
|
|
418
|
+
int: -1 if database is behind, 0 if up to date.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
PumSchemaMigrationError: If there is a mismatch between the database and the source.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
current_version = self.baseline(connection=connection)
|
|
425
|
+
migration_details = self.migration_details(connection=connection)
|
|
426
|
+
changelogs = [str(changelog.version) for changelog in self.config.changelogs()]
|
|
427
|
+
|
|
428
|
+
# Check if the current migration version is in the changelogs
|
|
429
|
+
if migration_details["version"] not in changelogs:
|
|
430
|
+
raise PumSchemaMigrationError(
|
|
431
|
+
f"Changelog for version {migration_details['version']} not found in the source."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Check if there are newer changelogs than current version
|
|
435
|
+
for changelog_version in changelogs:
|
|
436
|
+
if packaging.version.parse(changelog_version) > current_version:
|
|
437
|
+
return -1 # database is behind
|
|
438
|
+
|
|
439
|
+
return 0 # database is up to date
|