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/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .changelog import Changelog
|
|
2
|
+
from .dumper import Dumper, DumpFormat
|
|
3
|
+
from .pum_config import PumConfig
|
|
4
|
+
from .hook import HookHandler, HookBase
|
|
5
|
+
from .parameter import ParameterDefinition, ParameterType
|
|
6
|
+
from .role_manager import RoleManager, Role, Permission, PermissionType
|
|
7
|
+
from .schema_migrations import SchemaMigrations
|
|
8
|
+
from .sql_content import SqlContent
|
|
9
|
+
from .upgrader import Upgrader
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Changelog",
|
|
13
|
+
"Dumper",
|
|
14
|
+
"DumpFormat",
|
|
15
|
+
"HookBase",
|
|
16
|
+
"HookHandler",
|
|
17
|
+
"ParameterDefinition",
|
|
18
|
+
"ParameterType",
|
|
19
|
+
"Permission",
|
|
20
|
+
"PermissionType",
|
|
21
|
+
"PumConfig",
|
|
22
|
+
"Role",
|
|
23
|
+
"RoleManager",
|
|
24
|
+
"SchemaMigrations",
|
|
25
|
+
"SqlContent",
|
|
26
|
+
"Upgrader",
|
|
27
|
+
]
|
pum/changelog.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from os import listdir
|
|
2
|
+
from os.path import basename
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from packaging.version import parse as parse_version
|
|
6
|
+
import psycopg
|
|
7
|
+
|
|
8
|
+
from .exceptions import PumInvalidChangelog, PumSqlError
|
|
9
|
+
from .sql_content import SqlContent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Changelog:
|
|
13
|
+
"""This class represent a changelog directory.
|
|
14
|
+
The directory name is the version of the changelog.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, dir):
|
|
18
|
+
"""Args:
|
|
19
|
+
dir (str): The directory where the changelog is located.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
self.dir = dir
|
|
23
|
+
self.version = parse_version(basename(dir))
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"<dir: {self.dir} (v: {self.version})>"
|
|
27
|
+
|
|
28
|
+
def files(self) -> list[Path]:
|
|
29
|
+
"""Get the ordered list of SQL files in the changelog directory.
|
|
30
|
+
This is not recursive, it only returns the files in the given changelog directory.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
list[Path]: A list of paths to the changelog files.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
files = [
|
|
37
|
+
self.dir / f
|
|
38
|
+
for f in listdir(self.dir)
|
|
39
|
+
if (self.dir / f).is_file() and f.endswith(".sql")
|
|
40
|
+
]
|
|
41
|
+
files.sort()
|
|
42
|
+
return files
|
|
43
|
+
|
|
44
|
+
def validate(self, parameters: dict | None = None) -> bool:
|
|
45
|
+
"""Validate the changelog directory.
|
|
46
|
+
This is done by checking if the directory exists and if it contains at least one SQL file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
parameters: The parameters to pass to the SQL files.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
PumInvalidChangelog: If the changelog directory does not exist or does not contain any SQL files.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
if not self.dir.is_dir():
|
|
56
|
+
raise PumInvalidChangelog(f"Changelog directory `{self.dir}` does not exist.")
|
|
57
|
+
files = self.files()
|
|
58
|
+
if not files:
|
|
59
|
+
raise PumInvalidChangelog(
|
|
60
|
+
f"Changelog directory `{self.dir}` does not contain any SQL files."
|
|
61
|
+
)
|
|
62
|
+
for file in files:
|
|
63
|
+
if not file.is_file():
|
|
64
|
+
raise PumInvalidChangelog(f"Changelog file `{file}` does not exist.")
|
|
65
|
+
if not file.suffix == ".sql":
|
|
66
|
+
raise PumInvalidChangelog(f"Changelog file `{file}` is not a SQL file.")
|
|
67
|
+
try:
|
|
68
|
+
SqlContent(file).validate(parameters=parameters)
|
|
69
|
+
except PumSqlError as e:
|
|
70
|
+
raise PumInvalidChangelog(
|
|
71
|
+
f"Changelog file `{file}` is not a valid SQL file."
|
|
72
|
+
) from e
|
|
73
|
+
|
|
74
|
+
if not self.version:
|
|
75
|
+
raise PumInvalidChangelog(
|
|
76
|
+
f"Changelog directory `{self.dir}` does not have a valid version."
|
|
77
|
+
)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def apply(
|
|
81
|
+
self,
|
|
82
|
+
connection: psycopg.Connection,
|
|
83
|
+
parameters: dict | None = None,
|
|
84
|
+
commit: bool = True,
|
|
85
|
+
) -> list[Path]:
|
|
86
|
+
"""Apply a changelog
|
|
87
|
+
This will execute all the files in the changelog directory.
|
|
88
|
+
The changelog directory is the one that contains the delta files.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
connection: Connection
|
|
92
|
+
The connection to the database
|
|
93
|
+
parameters: dict
|
|
94
|
+
The parameters to pass to the SQL files
|
|
95
|
+
commit: bool
|
|
96
|
+
If true, the transaction is committed. The default is true.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
list[Path]
|
|
100
|
+
The list of changelogs that were executed
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
files = self.files()
|
|
104
|
+
for file in files:
|
|
105
|
+
try:
|
|
106
|
+
SqlContent(file).execute(
|
|
107
|
+
connection=connection, commit=commit, parameters=parameters
|
|
108
|
+
)
|
|
109
|
+
except PumSqlError as e:
|
|
110
|
+
raise PumSqlError(f"Error applying changelog {file}: {e}") from e
|
|
111
|
+
return files
|
pum/checker.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
|
|
3
|
+
import psycopg
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Checker:
|
|
7
|
+
"""This class is used to compare 2 Postgres databases and show the
|
|
8
|
+
differences.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
pg_service1,
|
|
14
|
+
pg_service2,
|
|
15
|
+
exclude_schema=None,
|
|
16
|
+
exclude_field_pattern=None,
|
|
17
|
+
ignore_list=None,
|
|
18
|
+
verbose_level=1,
|
|
19
|
+
):
|
|
20
|
+
"""Constructor
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
pg_service1: str
|
|
25
|
+
The name of the postgres service (defined in pg_service.conf)
|
|
26
|
+
related to the first db to be compared
|
|
27
|
+
pg_service2: str
|
|
28
|
+
The name of the postgres service (defined in pg_service.conf)
|
|
29
|
+
related to the first db to be compared
|
|
30
|
+
ignore_list: list(str)
|
|
31
|
+
List of elements to be ignored in check (ex. tables, columns,
|
|
32
|
+
views, ...)
|
|
33
|
+
exclude_schema: list of strings
|
|
34
|
+
List of schemas to be ignored in check.
|
|
35
|
+
exclude_field_pattern: list of strings
|
|
36
|
+
List of field patterns to be ignored in check.
|
|
37
|
+
verbose_level: int
|
|
38
|
+
verbose level, 0 -> nothing, 1 -> print first 80 char of each
|
|
39
|
+
difference, 2 -> print all the difference details
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
self.conn1 = psycopg.connect(f"service={pg_service1}")
|
|
43
|
+
self.cur1 = self.conn1.cursor()
|
|
44
|
+
|
|
45
|
+
self.conn2 = psycopg.connect(f"service={pg_service2}")
|
|
46
|
+
self.cur2 = self.conn2.cursor()
|
|
47
|
+
|
|
48
|
+
self.ignore_list = ignore_list
|
|
49
|
+
self.exclude_schema = "('information_schema'"
|
|
50
|
+
if exclude_schema is not None:
|
|
51
|
+
for schema in exclude_schema:
|
|
52
|
+
self.exclude_schema += f", '{schema}'"
|
|
53
|
+
self.exclude_schema += ")"
|
|
54
|
+
self.exclude_field_pattern = exclude_field_pattern or []
|
|
55
|
+
|
|
56
|
+
self.verbose_level = verbose_level
|
|
57
|
+
|
|
58
|
+
def run_checks(self):
|
|
59
|
+
"""Run all the checks functions.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
bool
|
|
64
|
+
True if all the checks are true
|
|
65
|
+
False otherwise
|
|
66
|
+
dict
|
|
67
|
+
Dictionary of lists of differences
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
result = True
|
|
71
|
+
differences_dict = {}
|
|
72
|
+
|
|
73
|
+
if "tables" not in self.ignore_list:
|
|
74
|
+
tmp_result, differences_dict["tables"] = self.check_tables()
|
|
75
|
+
result = False if not tmp_result else result
|
|
76
|
+
if "columns" not in self.ignore_list:
|
|
77
|
+
tmp_result, differences_dict["columns"] = self.check_columns(
|
|
78
|
+
"views" not in self.ignore_list
|
|
79
|
+
)
|
|
80
|
+
result = False if not tmp_result else result
|
|
81
|
+
if "constraints" not in self.ignore_list:
|
|
82
|
+
tmp_result, differences_dict["constraints"] = self.check_constraints()
|
|
83
|
+
result = False if not tmp_result else result
|
|
84
|
+
if "views" not in self.ignore_list:
|
|
85
|
+
tmp_result, differences_dict["views"] = self.check_views()
|
|
86
|
+
result = False if not tmp_result else result
|
|
87
|
+
if "sequences" not in self.ignore_list:
|
|
88
|
+
tmp_result, differences_dict["sequences"] = self.check_sequences()
|
|
89
|
+
result = False if not tmp_result else result
|
|
90
|
+
if "indexes" not in self.ignore_list:
|
|
91
|
+
tmp_result, differences_dict["indexes"] = self.check_indexes()
|
|
92
|
+
result = False if not tmp_result else result
|
|
93
|
+
if "triggers" not in self.ignore_list:
|
|
94
|
+
tmp_result, differences_dict["triggers"] = self.check_triggers()
|
|
95
|
+
result = False if not tmp_result else result
|
|
96
|
+
if "functions" not in self.ignore_list:
|
|
97
|
+
tmp_result, differences_dict["functions"] = self.check_functions()
|
|
98
|
+
result = False if not tmp_result else result
|
|
99
|
+
if "rules" not in self.ignore_list:
|
|
100
|
+
tmp_result, differences_dict["rules"] = self.check_rules()
|
|
101
|
+
result = False if not tmp_result else result
|
|
102
|
+
if self.verbose_level == 0:
|
|
103
|
+
differences_dict = None
|
|
104
|
+
return result, differences_dict
|
|
105
|
+
|
|
106
|
+
def check_tables(self):
|
|
107
|
+
"""Check if the tables are equals.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
bool
|
|
112
|
+
True if the tables are the same
|
|
113
|
+
False otherwise
|
|
114
|
+
list
|
|
115
|
+
A list with the differences
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
query = rf"""SELECT table_schema, table_name
|
|
119
|
+
FROM information_schema.tables
|
|
120
|
+
WHERE table_schema NOT IN {self.exclude_schema}
|
|
121
|
+
AND table_schema NOT LIKE 'pg\_%'
|
|
122
|
+
AND table_type NOT LIKE 'VIEW'
|
|
123
|
+
ORDER BY table_schema, table_name
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
return self.__check_equals(query)
|
|
127
|
+
|
|
128
|
+
def check_columns(self, check_views=True):
|
|
129
|
+
"""Check if the columns in all tables are equals.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
check_views: bool
|
|
134
|
+
if True, check the columns of all the tables and views, if
|
|
135
|
+
False check only the columns of the tables
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
bool
|
|
140
|
+
True if the columns are the same
|
|
141
|
+
False otherwise
|
|
142
|
+
list
|
|
143
|
+
A list with the differences
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
with_query = None
|
|
147
|
+
if check_views:
|
|
148
|
+
with_query = rf"""WITH table_list AS (
|
|
149
|
+
SELECT table_schema, table_name
|
|
150
|
+
FROM information_schema.tables
|
|
151
|
+
WHERE table_schema NOT IN {self.exclude_schema}
|
|
152
|
+
AND table_schema NOT LIKE 'pg\_%'
|
|
153
|
+
ORDER BY table_schema,table_name
|
|
154
|
+
)"""
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
with_query = rf"""WITH table_list AS (
|
|
158
|
+
SELECT table_schema, table_name
|
|
159
|
+
FROM information_schema.tables
|
|
160
|
+
WHERE table_schema NOT IN {self.exclude_schema}
|
|
161
|
+
AND table_schema NOT LIKE 'pg\_%'
|
|
162
|
+
AND table_type NOT LIKE 'VIEW'
|
|
163
|
+
ORDER BY table_schema,table_name
|
|
164
|
+
)"""
|
|
165
|
+
|
|
166
|
+
query = """{wq}
|
|
167
|
+
SELECT isc.table_schema, isc.table_name, column_name,
|
|
168
|
+
column_default, is_nullable, data_type,
|
|
169
|
+
character_maximum_length::text, numeric_precision::text,
|
|
170
|
+
numeric_precision_radix::text, datetime_precision::text
|
|
171
|
+
FROM information_schema.columns isc,
|
|
172
|
+
table_list tl
|
|
173
|
+
WHERE isc.table_schema = tl.table_schema
|
|
174
|
+
AND isc.table_name = tl.table_name
|
|
175
|
+
{efp}
|
|
176
|
+
ORDER BY isc.table_schema, isc.table_name, column_name
|
|
177
|
+
""".format(
|
|
178
|
+
wq=with_query,
|
|
179
|
+
efp="".join(
|
|
180
|
+
[f" AND column_name NOT LIKE '{pattern}'" for pattern in self.exclude_field_pattern]
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return self.__check_equals(query)
|
|
185
|
+
|
|
186
|
+
def check_constraints(self):
|
|
187
|
+
"""Check if the constraints are equals.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
bool
|
|
192
|
+
True if the constraints are the same
|
|
193
|
+
False otherwise
|
|
194
|
+
list
|
|
195
|
+
A list with the differences
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
query = f""" select
|
|
199
|
+
tc.constraint_name,
|
|
200
|
+
tc.constraint_schema || '.' || tc.table_name || '.' ||
|
|
201
|
+
kcu.column_name as physical_full_name,
|
|
202
|
+
tc.constraint_schema,
|
|
203
|
+
tc.table_name,
|
|
204
|
+
kcu.column_name,
|
|
205
|
+
ccu.table_name as foreign_table_name,
|
|
206
|
+
ccu.column_name as foreign_column_name,
|
|
207
|
+
tc.constraint_type
|
|
208
|
+
from information_schema.table_constraints as tc
|
|
209
|
+
join information_schema.key_column_usage as kcu on
|
|
210
|
+
(tc.constraint_name = kcu.constraint_name and
|
|
211
|
+
tc.table_name = kcu.table_name)
|
|
212
|
+
join information_schema.constraint_column_usage as ccu on
|
|
213
|
+
ccu.constraint_name = tc.constraint_name
|
|
214
|
+
WHERE tc.constraint_schema NOT IN {self.exclude_schema}
|
|
215
|
+
ORDER BY tc.constraint_schema, physical_full_name,
|
|
216
|
+
tc.constraint_name, foreign_table_name,
|
|
217
|
+
foreign_column_name """
|
|
218
|
+
|
|
219
|
+
return self.__check_equals(query)
|
|
220
|
+
|
|
221
|
+
def check_views(self):
|
|
222
|
+
"""Check if the views are equals.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
bool
|
|
227
|
+
True if the views are the same
|
|
228
|
+
False otherwise
|
|
229
|
+
list
|
|
230
|
+
A list with the differences
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
query = rf"""
|
|
234
|
+
SELECT table_name, REPLACE(view_definition,'"','')
|
|
235
|
+
FROM INFORMATION_SCHEMA.views
|
|
236
|
+
WHERE table_schema NOT IN {self.exclude_schema}
|
|
237
|
+
AND table_schema NOT LIKE 'pg\_%'
|
|
238
|
+
AND table_name not like 'vw_export_%'
|
|
239
|
+
ORDER BY table_schema, table_name
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
return self.__check_equals(query)
|
|
243
|
+
|
|
244
|
+
def check_sequences(self):
|
|
245
|
+
"""Check if the sequences are equals.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
bool
|
|
250
|
+
True if the sequences are the same
|
|
251
|
+
False otherwise
|
|
252
|
+
list
|
|
253
|
+
A list with the differences
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
query = f"""
|
|
257
|
+
SELECT c.relname,
|
|
258
|
+
ns.nspname as schema_name
|
|
259
|
+
FROM pg_class c
|
|
260
|
+
JOIN pg_namespace ns ON c.relnamespace = ns.oid
|
|
261
|
+
WHERE c.relkind = 'S'
|
|
262
|
+
AND ns.nspname NOT IN {self.exclude_schema}
|
|
263
|
+
ORDER BY c.relname"""
|
|
264
|
+
|
|
265
|
+
return self.__check_equals(query)
|
|
266
|
+
|
|
267
|
+
def check_indexes(self):
|
|
268
|
+
"""Check if the indexes are equals.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
bool
|
|
273
|
+
True if the indexes are the same
|
|
274
|
+
False otherwise
|
|
275
|
+
list
|
|
276
|
+
A list with the differences
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
query = rf"""
|
|
280
|
+
select
|
|
281
|
+
t.relname as table_name,
|
|
282
|
+
i.relname as index_name,
|
|
283
|
+
a.attname as column_name,
|
|
284
|
+
ns.nspname as schema_name
|
|
285
|
+
from
|
|
286
|
+
pg_class t,
|
|
287
|
+
pg_class i,
|
|
288
|
+
pg_index ix,
|
|
289
|
+
pg_attribute a,
|
|
290
|
+
pg_namespace ns
|
|
291
|
+
where
|
|
292
|
+
t.oid = ix.indrelid
|
|
293
|
+
and i.oid = ix.indexrelid
|
|
294
|
+
and a.attrelid = t.oid
|
|
295
|
+
and t.relnamespace = ns.oid
|
|
296
|
+
and a.attnum = ANY(ix.indkey)
|
|
297
|
+
and t.relkind = 'r'
|
|
298
|
+
AND t.relname NOT IN ('information_schema')
|
|
299
|
+
AND t.relname NOT LIKE 'pg\_%'
|
|
300
|
+
AND ns.nspname NOT IN {self.exclude_schema}
|
|
301
|
+
order by
|
|
302
|
+
t.relname,
|
|
303
|
+
i.relname,
|
|
304
|
+
a.attname
|
|
305
|
+
"""
|
|
306
|
+
return self.__check_equals(query)
|
|
307
|
+
|
|
308
|
+
def check_triggers(self) -> dict:
|
|
309
|
+
"""Check if the triggers are equals.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
bool
|
|
314
|
+
True if the triggers are the same
|
|
315
|
+
False otherwise
|
|
316
|
+
list
|
|
317
|
+
A list with the differences
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
query = f"""
|
|
321
|
+
WITH trigger_list AS (
|
|
322
|
+
select tgname, tgisinternal from pg_trigger
|
|
323
|
+
GROUP BY tgname, tgisinternal
|
|
324
|
+
)
|
|
325
|
+
select ns.nspname as schema_name, p.relname, t.tgname, pp.prosrc
|
|
326
|
+
from pg_trigger t, pg_proc pp, trigger_list tl, pg_class p, pg_namespace ns
|
|
327
|
+
where pp.oid = t.tgfoid
|
|
328
|
+
and t.tgname = tl.tgname
|
|
329
|
+
AND t.tgrelid = p.oid
|
|
330
|
+
AND p.relnamespace = ns.oid
|
|
331
|
+
AND NOT tl.tgisinternal
|
|
332
|
+
AND ns.nspname NOT IN {self.exclude_schema}
|
|
333
|
+
ORDER BY p.relname, t.tgname, pp.prosrc"""
|
|
334
|
+
|
|
335
|
+
return self.__check_equals(query)
|
|
336
|
+
|
|
337
|
+
def check_functions(self):
|
|
338
|
+
"""Check if the functions are equals.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
bool
|
|
343
|
+
True if the functions are the same
|
|
344
|
+
False otherwise
|
|
345
|
+
list
|
|
346
|
+
A list with the differences
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
query = rf"""
|
|
350
|
+
SELECT routines.routine_schema, routines.routine_name, parameters.data_type,
|
|
351
|
+
routines.routine_definition
|
|
352
|
+
FROM information_schema.routines
|
|
353
|
+
LEFT JOIN information_schema.parameters
|
|
354
|
+
ON routines.specific_name=parameters.specific_name
|
|
355
|
+
WHERE routines.specific_schema NOT IN {self.exclude_schema}
|
|
356
|
+
AND routines.specific_schema NOT LIKE 'pg\_%'
|
|
357
|
+
AND routines.specific_schema <> 'information_schema'
|
|
358
|
+
ORDER BY routines.routine_name, parameters.data_type,
|
|
359
|
+
routines.routine_definition, parameters.ordinal_position
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
return self.__check_equals(query)
|
|
363
|
+
|
|
364
|
+
def check_rules(self):
|
|
365
|
+
"""Check if the rules are equals.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
bool
|
|
370
|
+
True if the rules are the same
|
|
371
|
+
False otherwise
|
|
372
|
+
list
|
|
373
|
+
A list with the differences
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
query = rf"""
|
|
377
|
+
select n.nspname as rule_schema,
|
|
378
|
+
c.relname as rule_table,
|
|
379
|
+
r.rulename as rule_name,
|
|
380
|
+
case r.ev_type
|
|
381
|
+
when '1' then 'SELECT'
|
|
382
|
+
when '2' then 'UPDATE'
|
|
383
|
+
when '3' then 'INSERT'
|
|
384
|
+
when '4' then 'DELETE'
|
|
385
|
+
else 'UNKNOWN'
|
|
386
|
+
end as rule_event
|
|
387
|
+
from pg_rewrite r
|
|
388
|
+
join pg_class c on r.ev_class = c.oid
|
|
389
|
+
left join pg_namespace n on n.oid = c.relnamespace
|
|
390
|
+
left join pg_description d on r.oid = d.objoid
|
|
391
|
+
WHERE n.nspname NOT IN {self.exclude_schema}
|
|
392
|
+
AND n.nspname NOT LIKE 'pg\_%'
|
|
393
|
+
ORDER BY n.nspname, c.relname, r.rulename, rule_event
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
return self.__check_equals(query)
|
|
397
|
+
|
|
398
|
+
def __check_equals(self, query):
|
|
399
|
+
"""Check if the query results on the two databases are equals.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
bool
|
|
404
|
+
True if the results are the same
|
|
405
|
+
False otherwise
|
|
406
|
+
list
|
|
407
|
+
A list with the differences
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
self.cur1.execute(query)
|
|
411
|
+
records1 = self.cur1.fetchall()
|
|
412
|
+
|
|
413
|
+
self.cur2.execute(query)
|
|
414
|
+
records2 = self.cur2.fetchall()
|
|
415
|
+
|
|
416
|
+
result = True
|
|
417
|
+
differences = []
|
|
418
|
+
|
|
419
|
+
d = difflib.Differ()
|
|
420
|
+
records1 = [str(x) for x in records1]
|
|
421
|
+
records2 = [str(x) for x in records2]
|
|
422
|
+
|
|
423
|
+
for line in d.compare(records1, records2):
|
|
424
|
+
if line[0] in ("-", "+"):
|
|
425
|
+
result = False
|
|
426
|
+
if self.verbose_level == 1:
|
|
427
|
+
differences.append(line[0:79])
|
|
428
|
+
elif self.verbose_level == 2:
|
|
429
|
+
differences.append(line)
|
|
430
|
+
|
|
431
|
+
return result, differences
|