pum 1.0.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.
- pum/__init__.py +27 -0
- pum/changelog.py +111 -0
- pum/checker.py +431 -0
- pum/cli.py +402 -0
- pum/conf/pum_config_example.yaml +19 -0
- pum/config_model.py +152 -0
- pum/dumper.py +110 -0
- pum/exceptions.py +47 -0
- pum/hook.py +231 -0
- pum/info.py +30 -0
- pum/parameter.py +72 -0
- pum/pum_config.py +231 -0
- pum/role_manager.py +253 -0
- pum/schema_migrations.py +306 -0
- pum/sql_content.py +265 -0
- pum/upgrader.py +188 -0
- pum-1.0.0.dist-info/METADATA +61 -0
- pum-1.0.0.dist-info/RECORD +22 -0
- pum-1.0.0.dist-info/WHEEL +5 -0
- pum-1.0.0.dist-info/entry_points.txt +2 -0
- pum-1.0.0.dist-info/licenses/LICENSE +339 -0
- pum-1.0.0.dist-info/top_level.txt +1 -0
pum/role_manager.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import copy
|
|
4
|
+
import psycopg
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .sql_content import SqlContent
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionType(enum.Enum):
|
|
13
|
+
"""Enum for permission types.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
READ (str): Read permission.
|
|
17
|
+
WRITE (str): Write permission.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
READ = "read"
|
|
21
|
+
WRITE = "write"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Permission:
|
|
25
|
+
"""Class to represent a permission for a database role.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
type: Type of permission (read or write).
|
|
29
|
+
schemas: List of schemas this permission applies to.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, type: PermissionType | str, schemas: list[str] = None) -> None:
|
|
33
|
+
if not isinstance(type, PermissionType):
|
|
34
|
+
type = PermissionType(type)
|
|
35
|
+
self.type = type
|
|
36
|
+
self.schemas = schemas
|
|
37
|
+
|
|
38
|
+
def grant(
|
|
39
|
+
self,
|
|
40
|
+
role: str,
|
|
41
|
+
connection: psycopg.Connection,
|
|
42
|
+
commit: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Grant the permission to the specified role.
|
|
45
|
+
Args:
|
|
46
|
+
role: The name of the role to grant the permission to.
|
|
47
|
+
connection: The database connection to execute the SQL statements.
|
|
48
|
+
commit: Whether to commit the transaction. Defaults to False.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(role, str):
|
|
51
|
+
raise TypeError("Role must be a string.")
|
|
52
|
+
|
|
53
|
+
if not self.schemas:
|
|
54
|
+
raise ValueError("Schemas must be defined for the permission.")
|
|
55
|
+
|
|
56
|
+
for schema in self.schemas:
|
|
57
|
+
logger.info(f"Granting {self.type.value} permission on schema {schema} to role {role}.")
|
|
58
|
+
if self.type == PermissionType.READ:
|
|
59
|
+
SqlContent("""
|
|
60
|
+
GRANT USAGE ON SCHEMA {schema} TO {role};
|
|
61
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA {schema} TO {role};
|
|
62
|
+
GRANT SELECT, REFERENCES, TRIGGER ON ALL TABLES IN SCHEMA {schema} TO {role};
|
|
63
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT SELECT, REFERENCES, TRIGGER ON TABLES TO {role};
|
|
64
|
+
""").execute(
|
|
65
|
+
connection=connection,
|
|
66
|
+
commit=False,
|
|
67
|
+
parameters={
|
|
68
|
+
"schema": psycopg.sql.Identifier(schema),
|
|
69
|
+
"role": psycopg.sql.Identifier(role),
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
elif self.type == PermissionType.WRITE:
|
|
73
|
+
SqlContent("""
|
|
74
|
+
GRANT ALL ON SCHEMA {schema} TO {role};
|
|
75
|
+
GRANT ALL ON ALL TABLES IN SCHEMA {schema} TO {role};
|
|
76
|
+
GRANT ALL ON ALL SEQUENCES IN SCHEMA {schema} TO {role};
|
|
77
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA {schema} GRANT ALL ON TABLES TO {role};
|
|
78
|
+
""").execute(
|
|
79
|
+
connection=connection,
|
|
80
|
+
commit=False,
|
|
81
|
+
parameters={
|
|
82
|
+
"schema": psycopg.sql.Identifier(schema),
|
|
83
|
+
"role": psycopg.sql.Identifier(role),
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unknown permission type: {self.type}")
|
|
88
|
+
|
|
89
|
+
if commit:
|
|
90
|
+
connection.commit()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Role:
|
|
94
|
+
""" "
|
|
95
|
+
Represents a database role with associated permissions and optional inheritance.
|
|
96
|
+
The Role class encapsulates the concept of a database role, including its name,
|
|
97
|
+
permissions, optional inheritance from another role, and an optional description.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
name: str,
|
|
103
|
+
permissions: list[Permission] | list[str],
|
|
104
|
+
*,
|
|
105
|
+
inherit: Optional["Role"] = None,
|
|
106
|
+
description: str | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize the Role class.
|
|
109
|
+
Args:
|
|
110
|
+
name: Name of the role.
|
|
111
|
+
permissions: List of permissions associated with the role.
|
|
112
|
+
inherit: Optional role to inherit permissions from.
|
|
113
|
+
description: Optional description of the role.
|
|
114
|
+
"""
|
|
115
|
+
self.name = name
|
|
116
|
+
if isinstance(permissions, list) and all(isinstance(p, dict) for p in permissions):
|
|
117
|
+
self._permissions = [Permission(**p) for p in permissions]
|
|
118
|
+
elif isinstance(permissions, list) and all(isinstance(p, Permission) for p in permissions):
|
|
119
|
+
self._permissions = permissions
|
|
120
|
+
else:
|
|
121
|
+
raise TypeError("Permissions must be a list of dictionnaries or Permission instances.")
|
|
122
|
+
|
|
123
|
+
if inherit is not None and not isinstance(inherit, Role):
|
|
124
|
+
raise TypeError("Inherit must be a Role instance or None.")
|
|
125
|
+
self.inherit = inherit
|
|
126
|
+
self.description = description
|
|
127
|
+
|
|
128
|
+
def permissions(self):
|
|
129
|
+
"""
|
|
130
|
+
Returns the list of permissions associated with the role.
|
|
131
|
+
"""
|
|
132
|
+
return self._permissions
|
|
133
|
+
|
|
134
|
+
def exists(self, connection: psycopg.Connection) -> bool:
|
|
135
|
+
"""Check if the role exists in the database.
|
|
136
|
+
Args:
|
|
137
|
+
connection: The database connection to execute the SQL statements.
|
|
138
|
+
Returns:
|
|
139
|
+
bool: True if the role exists, False otherwise.
|
|
140
|
+
"""
|
|
141
|
+
SqlContent("SELECT 1 FROM pg_roles WHERE rolname = {name}").execute(
|
|
142
|
+
connection=connection,
|
|
143
|
+
commit=False,
|
|
144
|
+
parameters={"name": psycopg.sql.Literal(self.name)},
|
|
145
|
+
).fetchone() is not None
|
|
146
|
+
|
|
147
|
+
def create(
|
|
148
|
+
self, connection: psycopg.Connection, grant: bool = False, commit: bool = False
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Create the role in the database.
|
|
151
|
+
Args:
|
|
152
|
+
connection: The database connection to execute the SQL statements.
|
|
153
|
+
grant: Whether to grant permissions to the role. Defaults to False.
|
|
154
|
+
commit: Whether to commit the transaction. Defaults to False.
|
|
155
|
+
"""
|
|
156
|
+
if self.exists(connection):
|
|
157
|
+
logger.info(f"Role {self.name} already exists, skipping creation.")
|
|
158
|
+
else:
|
|
159
|
+
logger.info(f"Creating role {self.name}.")
|
|
160
|
+
SqlContent(
|
|
161
|
+
"CREATE ROLE {name} NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION"
|
|
162
|
+
).execute(
|
|
163
|
+
connection=connection,
|
|
164
|
+
commit=False,
|
|
165
|
+
parameters={"name": psycopg.sql.Identifier(self.name)},
|
|
166
|
+
)
|
|
167
|
+
if self.description:
|
|
168
|
+
SqlContent("COMMENT ON ROLE {name} IS {description}").execute(
|
|
169
|
+
connection=connection,
|
|
170
|
+
commit=False,
|
|
171
|
+
parameters={
|
|
172
|
+
"name": psycopg.sql.Identifier(self.name),
|
|
173
|
+
"description": psycopg.sql.Literal(self.description),
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
if self.inherit:
|
|
177
|
+
SqlContent("GRANT {inherit} TO {role}").execute(
|
|
178
|
+
connection=connection,
|
|
179
|
+
commit=False,
|
|
180
|
+
parameters={
|
|
181
|
+
"inherit": psycopg.sql.Identifier(self.inherit.name),
|
|
182
|
+
"role": psycopg.sql.Identifier(self.name),
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
if grant:
|
|
186
|
+
for permission in self.permissions():
|
|
187
|
+
permission.grant(role=self.name, connection=connection, commit=commit)
|
|
188
|
+
|
|
189
|
+
if commit:
|
|
190
|
+
connection.commit()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RoleManager:
|
|
194
|
+
"""
|
|
195
|
+
RoleManager manages a collection of Role objects,
|
|
196
|
+
allowing creation and permission management
|
|
197
|
+
for multiple roles in the PostgreSQL database.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, roles=list[Role] | list[dict]) -> None:
|
|
201
|
+
"""Initialize the RoleManager class.:
|
|
202
|
+
Args:
|
|
203
|
+
roles: List of roles or dictionaries defining roles.
|
|
204
|
+
Each role can be a dictionary with keys 'name', 'permissions', and optional 'description' and 'inherit'.
|
|
205
|
+
"""
|
|
206
|
+
if isinstance(roles, list) and all(isinstance(role, dict) for role in roles):
|
|
207
|
+
self.roles = {}
|
|
208
|
+
for role in roles:
|
|
209
|
+
_inherit = role.get("inherit")
|
|
210
|
+
if _inherit is not None:
|
|
211
|
+
if _inherit not in self.roles:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Inherited role {_inherit} does not exist in the already defined roles. Pay attention to the order of the roles in the list."
|
|
214
|
+
)
|
|
215
|
+
role["inherit"] = self.roles[_inherit]
|
|
216
|
+
self.roles[role["name"]] = Role(**role)
|
|
217
|
+
elif isinstance(roles, list) and all(isinstance(role, Role) for role in roles):
|
|
218
|
+
_roles = copy.deepcopy(roles)
|
|
219
|
+
self.roles = {role.name: role for role in _roles}
|
|
220
|
+
else:
|
|
221
|
+
raise TypeError("Roles must be a list of dictionaries or Role instances.")
|
|
222
|
+
|
|
223
|
+
for role in self.roles.values():
|
|
224
|
+
if role.inherit is not None and role.inherit not in self.roles.values():
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Inherited role {role.inherit.name} does not exist in the defined roles."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def create_roles(
|
|
230
|
+
self, connection: psycopg.Connection, grant: bool = False, commit: bool = False
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Create roles in the database.
|
|
233
|
+
Args:
|
|
234
|
+
connection: The database connection to execute the SQL statements.
|
|
235
|
+
grant: Whether to grant permissions to the roles. Defaults to False.
|
|
236
|
+
commit: Whether to commit the transaction. Defaults to False.
|
|
237
|
+
"""
|
|
238
|
+
for role in self.roles.values():
|
|
239
|
+
role.create(connection=connection, commit=False, grant=grant)
|
|
240
|
+
if commit:
|
|
241
|
+
connection.commit()
|
|
242
|
+
|
|
243
|
+
def grant_permissions(self, connection: psycopg.Connection, commit: bool = False) -> None:
|
|
244
|
+
"""Grant permissions to the roles in the database.
|
|
245
|
+
Args:
|
|
246
|
+
connection: The database connection to execute the SQL statements.
|
|
247
|
+
commit: Whether to commit the transaction. Defaults to False.
|
|
248
|
+
"""
|
|
249
|
+
for role in self.roles.values():
|
|
250
|
+
for permission in role.permissions():
|
|
251
|
+
permission.grant(role=role.name, connection=connection, commit=False)
|
|
252
|
+
if commit:
|
|
253
|
+
connection.commit()
|
pum/schema_migrations.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import packaging
|
|
6
|
+
import psycopg
|
|
7
|
+
import psycopg.sql
|
|
8
|
+
|
|
9
|
+
from .pum_config import PumConfig
|
|
10
|
+
from .exceptions import PumException
|
|
11
|
+
from .sql_content import SqlContent
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
MIGRATION_TABLE_VERSION = "2025.0"
|
|
16
|
+
MIGRATION_TABLE_NAME = "pum_migrations"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SchemaMigrations:
|
|
20
|
+
"""Manage the schema migrations in the database.
|
|
21
|
+
It provides methods to create the schema_migrations table, check its existence,
|
|
22
|
+
set the baseline version, and retrieve migration details.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: PumConfig) -> None:
|
|
26
|
+
"""Initialize the SchemaMigrations class with a database connection and configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config (PumConfig): An instance of the PumConfig class containing configuration settings for the PUM system.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
self.config = config
|
|
33
|
+
self.migration_table_identifier = psycopg.sql.SQL(".").join(
|
|
34
|
+
[
|
|
35
|
+
psycopg.sql.Identifier(self.config.config.pum.migration_table_schema),
|
|
36
|
+
psycopg.sql.Identifier(MIGRATION_TABLE_NAME),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def exists(self, connection: psycopg.Connection) -> bool:
|
|
41
|
+
"""Check if the schema_migrations information table exists.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
connection: The database connection to check for the existence of the table.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if the table exists, False otherwise.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
query = psycopg.sql.SQL(
|
|
51
|
+
"""
|
|
52
|
+
SELECT EXISTS (
|
|
53
|
+
SELECT 1
|
|
54
|
+
FROM information_schema.tables
|
|
55
|
+
WHERE table_name = 'pum_migrations' AND table_schema = {schema}
|
|
56
|
+
);
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parameters = {
|
|
61
|
+
"schema": psycopg.sql.Literal(self.config.config.pum.migration_table_schema),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
65
|
+
return cursor.fetchone()[0]
|
|
66
|
+
|
|
67
|
+
def exists_in_other_schemas(self, connection: psycopg.Connection) -> list[str]:
|
|
68
|
+
"""Check if the schema_migrations information table exists in other schemas.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
connection: The database connection to check for the existence of the table.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List[str]: List of schemas where the table exists.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
query = psycopg.sql.SQL(
|
|
78
|
+
"""
|
|
79
|
+
SELECT table_schema
|
|
80
|
+
FROM information_schema.tables
|
|
81
|
+
WHERE table_name = 'pum_migrations' AND table_schema != {schema}
|
|
82
|
+
"""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parameters = {
|
|
86
|
+
"schema": psycopg.sql.Literal(self.config.config.pum.migration_table_schema),
|
|
87
|
+
}
|
|
88
|
+
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
89
|
+
return [row[0] for row in cursor.fetchall()]
|
|
90
|
+
|
|
91
|
+
def create(
|
|
92
|
+
self,
|
|
93
|
+
connection: psycopg.Connection,
|
|
94
|
+
*,
|
|
95
|
+
allow_multiple_schemas: bool = False,
|
|
96
|
+
commit: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Create the schema_migrations information table
|
|
99
|
+
Args:
|
|
100
|
+
connection: The database connection to create the table.
|
|
101
|
+
commit: If true, the transaction is committed. The default is false.
|
|
102
|
+
allow_multiple_schemas: If true, several pum_migrations tables are allowed in
|
|
103
|
+
distinct schemas. Default is false.
|
|
104
|
+
"""
|
|
105
|
+
if self.exists(connection):
|
|
106
|
+
logger.info(
|
|
107
|
+
f"{self.config.config.pum.migration_table_schema}.pum_migrations table already exists."
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not allow_multiple_schemas and len(self.exists_in_other_schemas(connection)) > 0:
|
|
112
|
+
raise PumException(
|
|
113
|
+
f"Another {self.config.config.pum.migration_table_schema}.{self.config.config.pum.migration_table_name} table exists in another schema (). "
|
|
114
|
+
"Please use the allow_multiple_schemas option to create a new one."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create the schema if it doesn't exist
|
|
118
|
+
parameters = {
|
|
119
|
+
"version": psycopg.sql.Literal(MIGRATION_TABLE_VERSION),
|
|
120
|
+
"schema": psycopg.sql.Identifier(self.config.config.pum.migration_table_schema),
|
|
121
|
+
"table": self.migration_table_identifier,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
create_schema_query = None
|
|
125
|
+
if self.config.config.pum.migration_table_schema != "public":
|
|
126
|
+
create_schema_query = psycopg.sql.SQL("CREATE SCHEMA IF NOT EXISTS {schema};")
|
|
127
|
+
|
|
128
|
+
create_table_query = psycopg.sql.SQL(
|
|
129
|
+
"""CREATE TABLE IF NOT EXISTS {table}
|
|
130
|
+
(
|
|
131
|
+
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
132
|
+
date_installed timestamp without time zone NOT NULL DEFAULT now(),
|
|
133
|
+
module character varying(50), -- TODO: NOT NULL,
|
|
134
|
+
version character varying(50) NOT NULL,
|
|
135
|
+
beta_testing boolean NOT NULL DEFAULT false,
|
|
136
|
+
changelog_files text[],
|
|
137
|
+
parameters jsonb,
|
|
138
|
+
migration_table_version character varying(50) NOT NULL DEFAULT {version}
|
|
139
|
+
);
|
|
140
|
+
"""
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
comment_query = psycopg.sql.SQL(
|
|
144
|
+
"COMMENT ON TABLE {table} IS 'version: 1 -- schema_migration table version';"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if create_schema_query:
|
|
148
|
+
SqlContent(create_schema_query).execute(connection, parameters=parameters)
|
|
149
|
+
SqlContent(create_table_query).execute(connection, parameters=parameters)
|
|
150
|
+
SqlContent(comment_query).execute(connection, parameters=parameters)
|
|
151
|
+
|
|
152
|
+
logger.info(f"Created {parameters['schema']}.{parameters['table']} table")
|
|
153
|
+
|
|
154
|
+
if commit:
|
|
155
|
+
connection.commit()
|
|
156
|
+
|
|
157
|
+
def set_baseline(
|
|
158
|
+
self,
|
|
159
|
+
connection: psycopg.Connection,
|
|
160
|
+
version: packaging.version.Version | str,
|
|
161
|
+
changelog_files: list[str] | None = None,
|
|
162
|
+
parameters: dict | None = None,
|
|
163
|
+
*,
|
|
164
|
+
beta_testing: bool = False,
|
|
165
|
+
commit: bool = False,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Set the baseline into the migration table.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
connection: The database connection to set the baseline version.
|
|
171
|
+
version: The version of the current database to set in the information.
|
|
172
|
+
changelog_files: The list of changelog files that were applied.
|
|
173
|
+
parameters: The parameters used in the migration.
|
|
174
|
+
beta_testing: If true, the baseline is set to beta testing mode. The default is false.
|
|
175
|
+
commit: If true, the transaction is committed. The default is False.
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
if isinstance(version, packaging.version.Version):
|
|
179
|
+
version = str(version)
|
|
180
|
+
pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
181
|
+
if not re.match(pattern, version):
|
|
182
|
+
raise ValueError(f"Wrong version format: {version}. Must be x.x.x")
|
|
183
|
+
|
|
184
|
+
current = self.baseline(connection=connection)
|
|
185
|
+
if current and current >= version:
|
|
186
|
+
raise PumException(f"Cannot set baseline {version} as it is already set at {current}.")
|
|
187
|
+
|
|
188
|
+
code = psycopg.sql.SQL("""
|
|
189
|
+
INSERT INTO {table} (
|
|
190
|
+
version,
|
|
191
|
+
beta_testing,
|
|
192
|
+
migration_table_version,
|
|
193
|
+
changelog_files,
|
|
194
|
+
parameters
|
|
195
|
+
) VALUES (
|
|
196
|
+
{version},
|
|
197
|
+
{beta_testing},
|
|
198
|
+
{migration_table_version},
|
|
199
|
+
{changelog_files},
|
|
200
|
+
{parameters}
|
|
201
|
+
);""")
|
|
202
|
+
|
|
203
|
+
query_parameters = {
|
|
204
|
+
"table": self.migration_table_identifier,
|
|
205
|
+
"version": psycopg.sql.Literal(version),
|
|
206
|
+
"beta_testing": psycopg.sql.Literal(beta_testing),
|
|
207
|
+
"migration_table_version": psycopg.sql.Literal(MIGRATION_TABLE_VERSION),
|
|
208
|
+
"changelog_files": psycopg.sql.Literal(changelog_files or []),
|
|
209
|
+
"parameters": psycopg.sql.Literal(json.dumps(parameters or {})),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Setting baseline version {version} in {self.config.config.pum.migration_table_schema}.{MIGRATION_TABLE_NAME} table"
|
|
214
|
+
)
|
|
215
|
+
SqlContent(code).execute(connection, parameters=query_parameters, commit=commit)
|
|
216
|
+
|
|
217
|
+
def baseline(self, connection: psycopg.Connection) -> str | None:
|
|
218
|
+
"""Return the baseline version from the migration table.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
connection: psycopg.Connection
|
|
222
|
+
The database connection to get the baseline version.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
str: The baseline version.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if not self.exists(connection=connection):
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
query = psycopg.sql.SQL(
|
|
233
|
+
"""
|
|
234
|
+
SELECT version
|
|
235
|
+
FROM {table}
|
|
236
|
+
WHERE id = (
|
|
237
|
+
SELECT id
|
|
238
|
+
FROM {table}
|
|
239
|
+
ORDER BY version DESC, date_installed DESC
|
|
240
|
+
LIMIT 1
|
|
241
|
+
)
|
|
242
|
+
"""
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
parameters = {
|
|
246
|
+
"table": self.migration_table_identifier,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
250
|
+
row = cursor.fetchone()
|
|
251
|
+
if row is None:
|
|
252
|
+
return None
|
|
253
|
+
return row[0]
|
|
254
|
+
|
|
255
|
+
def migration_details(self, connection: psycopg.Connection, version: str | None = None) -> dict:
|
|
256
|
+
"""Return the migration details from the migration table.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
connection:
|
|
260
|
+
The database connection to get the migration details.
|
|
261
|
+
version:
|
|
262
|
+
The version of the migration to get details for.
|
|
263
|
+
If None, last migration is returned.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
dict: The migration details.
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
query = None
|
|
270
|
+
if version is None:
|
|
271
|
+
query = psycopg.sql.SQL(
|
|
272
|
+
"""
|
|
273
|
+
SELECT *
|
|
274
|
+
FROM {table}
|
|
275
|
+
WHERE id = (
|
|
276
|
+
SELECT id
|
|
277
|
+
FROM {table}
|
|
278
|
+
ORDER BY version DESC, date_installed DESC
|
|
279
|
+
LIMIT 1
|
|
280
|
+
)
|
|
281
|
+
ORDER BY date_installed DESC
|
|
282
|
+
"""
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
parameters = {
|
|
286
|
+
"table": self.migration_table_identifier,
|
|
287
|
+
}
|
|
288
|
+
else:
|
|
289
|
+
query = psycopg.sql.SQL(
|
|
290
|
+
"""
|
|
291
|
+
SELECT *
|
|
292
|
+
FROM {table}
|
|
293
|
+
WHERE version = {version}
|
|
294
|
+
"""
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
parameters = {
|
|
298
|
+
"table": self.migration_table_identifier,
|
|
299
|
+
"version": psycopg.sql.Literal(version),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
cursor = SqlContent(query).execute(connection, parameters=parameters)
|
|
303
|
+
row = cursor.fetchone()
|
|
304
|
+
if row is None:
|
|
305
|
+
return None
|
|
306
|
+
return dict(zip([desc[0] for desc in cursor.description], row, strict=False))
|