psql-vcs 0.0.1a0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: psql_vcs
3
+ Version: 0.0.1a0
4
+ Summary: Postgres version control system
5
+ Author-email: Beloborod <me@beloborod.ru>
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: results>=2.0.1783086339
18
+ Requires-Dist: psycopg-binary>=3.3.4
19
+ Requires-Dist: psycopg>=3.3.4
20
+ Requires-Dist: pydantic>=2.13.4
21
+ Requires-Dist: psycopg-pool>=3.3.1
File without changes
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=83.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "psql_vcs"
7
+ description = "Postgres version control system"
8
+ version = "0.0.1a0"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [
12
+ { name = "Beloborod", email = "me@beloborod.ru" }
13
+ ]
14
+ dependencies = [
15
+ "results>=2.0.1783086339",
16
+ "psycopg-binary>=3.3.4",
17
+ "psycopg>=3.3.4",
18
+ "pydantic>=2.13.4",
19
+ "psycopg-pool>=3.3.1",
20
+ ]
21
+ license = { file = "LICENSE.txt" }
22
+ classifiers = [
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Operating System :: OS Independent",
32
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ from .modules import setup_logger, PostgresMigrator
2
+ from importlib.metadata import version, PackageNotFoundError
3
+
4
+
5
+ setup_logger()
6
+
7
+ __all__ = [
8
+ 'PostgresMigrator'
9
+ ]
10
+
11
+ try:
12
+ __version__ = version(__name__)
13
+ except PackageNotFoundError:
14
+ __version__ = "0.0.1a0"
@@ -0,0 +1,9 @@
1
+ from .migrator_args import AuthArgs, URLArgs
2
+ from .schemas import CurrentSchema
3
+
4
+
5
+ __all__ = [
6
+ 'AuthArgs',
7
+ 'URLArgs',
8
+ 'CurrentSchema',
9
+ ]
@@ -0,0 +1,63 @@
1
+ from ipaddress import IPv4Address
2
+ from pydantic import PostgresDsn
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class AuthArgs:
8
+ migration_server_host: str | IPv4Address
9
+ migration_server_port: int
10
+ migration_server_username: str
11
+ migration_server_password: str
12
+ target_database: str
13
+ target_server_host: str | IPv4Address | None = None
14
+ target_server_port: int | None = None
15
+ target_server_username: str | None = None
16
+ target_server_password: str | None = None
17
+ migration_server_main_database: str | None = None
18
+ migration_server_migrations_database: str | None = None
19
+ migration_server_test_database: str | None = None
20
+ target_server_main_database: str | None = None
21
+ migration_name: str | None = None
22
+
23
+ def __post_init__(self):
24
+ if self.target_server_host is None:
25
+ self.target_server_host = self.migration_server_host
26
+ if self.target_server_port is None:
27
+ self.target_server_port = self.migration_server_port
28
+ if self.target_server_username is None:
29
+ self.target_server_username = self.migration_server_username
30
+ if self.target_server_password is None:
31
+ self.target_server_password = self.migration_server_password
32
+ if self.migration_server_main_database is None:
33
+ self.migration_server_main_database = 'postgres'
34
+ if self.migration_server_migrations_database is None:
35
+ self.migration_server_migrations_database = 'psql_vcs_migrations_db'
36
+ if self.migration_server_test_database is None:
37
+ self.migration_server_test_database = 'psql_vcs_test_db'
38
+ if self.target_server_main_database is None:
39
+ self.target_server_main_database = self.migration_server_main_database
40
+ if self.migration_name is None:
41
+ self.migration_name = self.target_database
42
+
43
+ def __repr__(self):
44
+ return f"<AuthArgs {id(self)}>"
45
+
46
+
47
+ @dataclass
48
+ class URLArgs:
49
+ migrations_main_database_url: str | PostgresDsn
50
+ migrations_database_url: str | PostgresDsn
51
+ migrations_test_database_url: str | PostgresDsn
52
+ target_database_url: str | PostgresDsn
53
+ target_server_main_database_url: str | PostgresDsn
54
+ migration_name: str | None = None
55
+
56
+ def __post_init__(self):
57
+ if self.migration_name is None:
58
+ dsn = PostgresDsn(self.target_database_url) \
59
+ if isinstance(self.target_database_url, str) else self.target_database_url
60
+ self.migration_name = dsn.path
61
+
62
+ def __repr__(self):
63
+ return f"<URLArgs {id(self)}>"
@@ -0,0 +1,5 @@
1
+ class CurrentSchema:
2
+ def __init__(self, name: str, current_version: int, max_version: int):
3
+ self.name = name
4
+ self.current_version = current_version
5
+ self.max_version = max_version
@@ -0,0 +1,10 @@
1
+ from .connector import PostgresRequester
2
+ from .postgres_schema_processing import PostgresMigrator
3
+ from .logger import setup_logger
4
+
5
+
6
+ __all__ = [
7
+ 'PostgresRequester',
8
+ 'PostgresMigrator',
9
+ 'setup_logger'
10
+ ]
@@ -0,0 +1,33 @@
1
+ from psycopg import connect, Connection, OperationalError, sql
2
+ from psycopg_pool import ConnectionPool
3
+ from pydantic import PostgresDsn
4
+
5
+
6
+ class PostgresRequester:
7
+ def __init__(self, database_url: str | PostgresDsn):
8
+
9
+ if isinstance(database_url, str):
10
+ database_url = PostgresDsn(database_url)
11
+
12
+ self._dsn = database_url.encoded_string()
13
+
14
+ try:
15
+ with connect(self._dsn, connect_timeout=5) as conn:
16
+ conn.execute("SELECT 1")
17
+ except OperationalError as e:
18
+ raise RuntimeError(f"Failed to connect to database: {e}") from None
19
+
20
+ self._pool = ConnectionPool(self._dsn, min_size=2, max_size=10, max_idle=300, max_lifetime=3600)
21
+ self._no_trigger_pool = ConnectionPool(self._dsn, min_size=1, max_size=4, max_idle=300, max_lifetime=3600)
22
+
23
+ def __del__(self):
24
+ if hasattr(self, '_pool'): self._pool.close()
25
+ if hasattr(self, '_no_trigger_pool'): self._no_trigger_pool.close()
26
+
27
+ def __repr__(self):
28
+ return f"<PostgresRequester host={self._dsn.split('@')[-1].split('/')[0]} password=***>"
29
+
30
+ def get_connection(self) -> Connection:
31
+ connection = self._pool.getconn()
32
+ connection.autocommit = True
33
+ return connection
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger():
5
+ logger = logging.getLogger(__name__)
6
+
7
+ logger.addHandler(logging.NullHandler())
@@ -0,0 +1,420 @@
1
+ import logging
2
+ from psycopg import Error as PsycopgError
3
+ from psycopg.rows import dict_row
4
+ from psycopg import sql
5
+ from psycopg.types.json import Jsonb
6
+ from typing import cast, LiteralString
7
+ from collections import defaultdict
8
+ import results
9
+ from pydantic import PostgresDsn
10
+ from ..models import AuthArgs, URLArgs, CurrentSchema
11
+ from . import PostgresRequester
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class PostgresMigrator:
18
+ def __init__(self, args: AuthArgs | URLArgs):
19
+ if isinstance(args, AuthArgs):
20
+ self.main_migrations_dsn_obj = PostgresDsn.build(
21
+ scheme="postgresql",
22
+ host=args.migration_server_host,
23
+ port=args.migration_server_port,
24
+ username=args.migration_server_username,
25
+ password=args.migration_server_password,
26
+ path=args.migration_server_main_database
27
+ )
28
+ self.migrations_dsn_obj = PostgresDsn.build(
29
+ scheme="postgresql",
30
+ host=args.migration_server_host,
31
+ port=args.migration_server_port,
32
+ username=args.migration_server_username,
33
+ password=args.migration_server_password,
34
+ path=args.migration_server_migrations_database
35
+ )
36
+ self.test_dsn_obj = PostgresDsn.build(
37
+ scheme="postgresql",
38
+ host=args.migration_server_host,
39
+ port=args.migration_server_port,
40
+ username=args.migration_server_username,
41
+ password=args.migration_server_password,
42
+ path=args.migration_server_test_database
43
+ )
44
+ self.target_dsn_obj = PostgresDsn.build(
45
+ scheme="postgresql",
46
+ host=args.target_server_host,
47
+ port=args.target_server_port,
48
+ username=args.target_server_username,
49
+ password=args.target_server_password,
50
+ path=args.target_database
51
+ )
52
+ self.target_main_dns_obj = PostgresDsn.build(
53
+ scheme="postgresql",
54
+ host=args.target_server_host,
55
+ port=args.target_server_port,
56
+ username=args.target_server_username,
57
+ password=args.target_server_password,
58
+ path=args.target_server_main_database
59
+ )
60
+ else:
61
+ self.main_migrations_dsn_obj = PostgresDsn(args.migrations_main_database_url) \
62
+ if isinstance(args.migrations_main_database_url, str) else args.migrations_main_database_url
63
+ self.migrations_dsn_obj = PostgresDsn(args.migrations_database_url) \
64
+ if isinstance(args.migrations_database_url, str) else args.migrations_database_url
65
+ self.test_dsn_obj = PostgresDsn(args.migrations_test_database_url) \
66
+ if isinstance(args.migrations_test_database_url, str) else args.migrations_test_database_url
67
+ self.target_dsn_obj = PostgresDsn(args.target_database_url) \
68
+ if isinstance(args.target_database_url, str) else args.target_database_url
69
+ self.target_main_dsn_obj = PostgresDsn(args.target_server_main_database_url) \
70
+ if isinstance(args.target_server_main_database_url, str) else args.target_server_main_database_url
71
+
72
+ self.migration_name = args.migration_name
73
+
74
+ main_migrations_requester = PostgresRequester(self.target_main_dns_obj)
75
+ with main_migrations_requester.get_connection() as connection:
76
+ with connection.cursor() as cursor:
77
+ cursor.execute(
78
+ """
79
+ SELECT EXISTS (
80
+ SELECT 1
81
+ FROM pg_database
82
+ WHERE datname = %s
83
+ );
84
+ """, (self.migrations_dsn_obj.path.lstrip('/'),)
85
+ )
86
+ exists = cursor.fetchone()[0]
87
+ if not exists:
88
+ cursor.execute(
89
+ sql.SQL(
90
+ """
91
+ CREATE DATABASE {};
92
+ """
93
+ ).format(sql.Identifier(self.migrations_dsn_obj.path.lstrip('/')))
94
+ )
95
+
96
+ requester = PostgresRequester(self.migrations_dsn_obj)
97
+ with requester.get_connection() as connection:
98
+ with connection.cursor() as cursor:
99
+ cursor.execute(
100
+ """
101
+ CREATE SCHEMA IF NOT EXISTS migrations;
102
+ """)
103
+ cursor.execute(
104
+ """
105
+ CREATE
106
+ EXTENSION IF NOT EXISTS "uuid-ossp";
107
+ """
108
+ )
109
+ cursor.execute(
110
+ """
111
+ CREATE TABLE IF NOT EXISTS migrations.schemas
112
+ (
113
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
114
+ name CHARACTER VARYING (40) NOT NULL,
115
+ step SMALLINT NOT NULL,
116
+ schema JSONB NOT NULL UNIQUE,
117
+ sql_request CHARACTER VARYING NOT NULL,
118
+ created_at TIMESTAMPTZ DEFAULT NOW
119
+ (
120
+ )
121
+ );
122
+ """
123
+ )
124
+
125
+
126
+
127
+ def _extract_schema(self) -> dict:
128
+ schema = {"tables": {}, "indexes": [], "foreign_keys": []}
129
+
130
+ requester = PostgresRequester(self.target_dsn_obj)
131
+
132
+ try:
133
+ with requester.get_connection() as connection:
134
+ with connection.cursor() as cursor:
135
+ cursor.execute("""
136
+ SELECT table_schema, table_name, column_name, data_type, character_maximum_length,
137
+ numeric_precision, numeric_scale, datetime_precision,
138
+ is_nullable, column_default, ordinal_position
139
+ FROM information_schema.columns
140
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
141
+ AND table_name NOT LIKE 'pg_%'
142
+ ORDER BY table_schema, table_name, ordinal_position
143
+ """)
144
+ tables = defaultdict(list)
145
+ for sch, tbl, col, dtype, max_char, precis, scale, date_precis, \
146
+ nullable, default, pos in cursor.fetchall():
147
+ tables[f"{sch}.{tbl}"].append({
148
+ "name": col,
149
+ "type": dtype.strip(),
150
+ "nullable": nullable == "YES",
151
+ "max_char": max_char if max_char else None,
152
+ "precision": precis if precis else None,
153
+ "scale": scale if scale else None,
154
+ "date_precision": date_precis if date_precis else None,
155
+ "default": default.strip() if default else None,
156
+ "position": pos
157
+ })
158
+ schema["tables"] = dict(tables)
159
+
160
+ cursor.execute("""
161
+ SELECT schemaname, tablename, indexname, indexdef
162
+ FROM pg_indexes
163
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
164
+ ORDER BY schemaname, tablename, indexname
165
+ """)
166
+ for sch, tbl, idx_name, idx_def in cursor.fetchall():
167
+ clean_def = " ".join(idx_def.split()).replace("public.", "")
168
+ schema["indexes"].append({
169
+ "table": f"{sch}.{tbl}",
170
+ "name": idx_name,
171
+ "definition": clean_def
172
+ })
173
+
174
+ cursor.execute("""
175
+ SELECT
176
+ tc.table_schema, tc.table_name, tc.constraint_name,
177
+ kcu.column_name,
178
+ ccu.table_schema AS fk_schema,
179
+ ccu.table_name AS fk_table,
180
+ ccu.column_name AS fk_column,
181
+ pc.confdeltype as pc_del_type,
182
+ pc.confupdtype as pc_upd_type
183
+ FROM information_schema.table_constraints tc
184
+ JOIN information_schema.key_column_usage kcu
185
+ ON tc.constraint_name = kcu.constraint_name
186
+ AND tc.table_schema = kcu.table_schema
187
+ JOIN information_schema.constraint_column_usage ccu
188
+ ON ccu.constraint_name = tc.constraint_name
189
+ JOIN pg_constraint pc
190
+ ON pc.conname = tc.constraint_name
191
+ WHERE tc.constraint_type = 'FOREIGN KEY'
192
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
193
+ ORDER BY tc.table_schema, tc.table_name, tc.constraint_name, kcu.ordinal_position
194
+ """)
195
+ fks: defaultdict[str, dict[str, list | str | None]] = defaultdict(
196
+ lambda: {"columns": [], "ref_table": None, "ref_columns": [], "del_upd": None})
197
+ for row in cursor.fetchall():
198
+ key = f"{row[0]}.{row[1]}.{row[2]}"
199
+ fks[key]["columns"].append(row[3])
200
+ fks[key]["ref_table"] = f"{row[4]}.{row[5]}"
201
+ fks[key]["ref_columns"].append(row[6])
202
+ fks[key]["del_upd"] = f"{row[7]}.{row[8]}"
203
+ for v in fks.values():
204
+ v["columns"].sort()
205
+ v["ref_columns"].sort()
206
+
207
+ schema["foreign_keys"] = [
208
+ {"constraint_name": k, **v} for k, v in sorted(fks.items())
209
+ ]
210
+ schema["foreign_keys"] = [
211
+ {"constraint_name": k, **v} for k, v in fks.items()
212
+ ]
213
+ except PsycopgError as e:
214
+ raise RuntimeError(f"Connect or request failed: {e}") from None
215
+
216
+ return schema
217
+
218
+ def _save_schema_diff(self, schema: dict, sql_request: str) -> None:
219
+ requester = PostgresRequester(self.migrations_dsn_obj)
220
+ with requester.get_connection() as connection:
221
+ with connection.cursor() as cursor:
222
+ cursor.execute(
223
+ """
224
+ INSERT INTO migrations.schemas (name, step, schema, sql_request)
225
+ VALUES
226
+ (
227
+ %s,
228
+ (
229
+ SELECT COALESCE(MAX(step), -1) + 1
230
+ FROM migrations.schemas
231
+ WHERE name = %s
232
+ ),
233
+ %s,
234
+ %s
235
+ );
236
+ """, (self.migration_name, self.migration_name, Jsonb(schema), sql_request)
237
+ )
238
+
239
+ def _schema_compare(self, schema: dict) -> CurrentSchema:
240
+ requester = PostgresRequester(self.migrations_dsn_obj)
241
+ with requester.get_connection() as connection:
242
+ with connection.cursor(row_factory=dict_row) as cursor:
243
+ cursor.execute(
244
+ """
245
+ SELECT name, step FROM migrations.schemas
246
+ WHERE schema = %s;
247
+ """, (Jsonb(schema),)
248
+ )
249
+ search_result = cursor.fetchone()
250
+ if search_result is None:
251
+ raise RuntimeError("Scheme not found")
252
+
253
+ cursor.execute(
254
+ """
255
+ SELECT MAX(step) as max_version FROM migrations.schemas
256
+ WHERE name = %s;
257
+ """, (search_result['name'],)
258
+ )
259
+ max_version = cursor.fetchone()['max_version']
260
+ return CurrentSchema(search_result['name'], search_result['step'], max_version)
261
+
262
+ def _generate_map(self, current_version: int, max_version: int) -> list[str]:
263
+ requester = PostgresRequester(self.migrations_dsn_obj)
264
+ if current_version < max_version:
265
+ with requester.get_connection() as connection:
266
+ with connection.cursor(row_factory=dict_row) as cursor:
267
+ cursor.execute(
268
+ """
269
+ SELECT step, sql_request
270
+ FROM migrations.schemas
271
+ WHERE name = %s
272
+ AND step > %s
273
+ AND step <= %s
274
+ ORDER BY step;
275
+ """, (self.migration_name, current_version, max_version)
276
+ )
277
+ search_result = cursor.fetchall()
278
+ if len(search_result) == 0:
279
+ raise RuntimeError("Can't find schemas for %s, started on %s and end on %s versions".format(
280
+ self.migration_name, current_version, max_version))
281
+ return [row['sql_request'] for row in search_result]
282
+ else:
283
+ logger.debug(f"Schema version {current_version} is already actual")
284
+ return []
285
+
286
+ def _get_migration_map_by_schema(self, schema: dict) -> list[str]:
287
+ current_schema = self._schema_compare(schema)
288
+ return self._generate_map(current_schema.current_version, current_schema.max_version)
289
+
290
+ def _get_migration_map(self, start_version: int = 1, end_version: int | None = None) -> list[str]:
291
+ requester = PostgresRequester(self.migrations_dsn_obj)
292
+ if end_version is None:
293
+ with requester.get_connection() as connection:
294
+ with connection.cursor(row_factory=dict_row) as cursor:
295
+ cursor.execute(
296
+ """
297
+ SELECT MAX(step) as max_step FROM migrations.schemas
298
+ WHERE name = %s
299
+ """, (self.migration_name,)
300
+ )
301
+ search_result = cursor.fetchone()
302
+ end_version = search_result['max_step']
303
+ return self._generate_map(start_version, end_version)
304
+
305
+ def migrate_to_last_version(self) -> None:
306
+ main_target_requester = PostgresRequester(self.target_main_dns_obj)
307
+ with main_target_requester.get_connection() as connection:
308
+ with connection.cursor() as cursor:
309
+ cursor.execute(
310
+ """
311
+ SELECT 1
312
+ FROM pg_database
313
+ WHERE datname = %s;
314
+ """, (self.target_dsn_obj.path.lstrip('/'),)
315
+ )
316
+ a = cursor.fetchone()
317
+ if a is None:
318
+ initial_sql = self._get_migration_map(start_version=-1, end_version=0)
319
+ if not initial_sql:
320
+ raise RuntimeError(f"Can't find database for {self.migration_name}, after try to create - can't "
321
+ f"find initial schema")
322
+ cursor.execute(
323
+ sql.SQL(
324
+ """
325
+ CREATE DATABASE {};
326
+ """
327
+ ).format(sql.Identifier(self.target_dsn_obj.path.lstrip('/')))
328
+ )
329
+ target_requester = PostgresRequester(self.target_dsn_obj)
330
+ with target_requester.get_connection() as target_connection:
331
+ with target_connection.cursor() as target_cursor:
332
+ for migration in initial_sql:
333
+ target_cursor.execute(cast(LiteralString, migration))
334
+
335
+ schema = self._extract_schema()
336
+ migration_map = self._get_migration_map_by_schema(schema)
337
+ requester = PostgresRequester(self.target_dsn_obj)
338
+ with requester.get_connection() as connection:
339
+ with connection.cursor() as cursor:
340
+ for migration in migration_map:
341
+ cursor.execute(cast(LiteralString, migration))
342
+
343
+ def create_migration(self) -> None:
344
+ migrations_requester = PostgresRequester(self.migrations_dsn_obj)
345
+ with migrations_requester.get_connection() as connection:
346
+ with connection.cursor() as cursor:
347
+ cursor.execute(
348
+ """
349
+ SELECT EXISTS (
350
+ SELECT 1
351
+ FROM migrations.schemas
352
+ WHERE name = %s
353
+ AND step = %s
354
+ )
355
+ """, (self.migration_name, 0)
356
+ )
357
+ exists = cursor.fetchone()[0]
358
+
359
+ main_database_requester = PostgresRequester(self.main_migrations_dsn_obj)
360
+ with main_database_requester.get_connection() as connection:
361
+ with connection.cursor() as cursor:
362
+ cursor.execute(
363
+ """
364
+ SELECT pg_terminate_backend(pid)
365
+ FROM pg_stat_activity
366
+ WHERE datname = %s
367
+ AND pid <> pg_backend_pid();
368
+ """, (self.test_dsn_obj.path.lstrip('/'),)
369
+ )
370
+ cursor.execute(
371
+ sql.SQL(
372
+ """
373
+ DROP DATABASE IF EXISTS {};
374
+ """
375
+ ).format(sql.Identifier(self.test_dsn_obj.path.lstrip('/')))
376
+ )
377
+ cursor.execute(
378
+ sql.SQL(
379
+ """
380
+ CREATE DATABASE {};
381
+ """
382
+ ).format(sql.Identifier(self.test_dsn_obj.path.lstrip('/')))
383
+ )
384
+ test_requester = PostgresRequester(self.test_dsn_obj)
385
+
386
+ if exists:
387
+ full_migration_chain = self._get_migration_map(-1)
388
+ with test_requester.get_connection() as connection:
389
+ with connection.cursor() as cursor:
390
+ for migration in full_migration_chain:
391
+ cursor.execute(cast(LiteralString, migration))
392
+
393
+ diff = (results.db(self.test_dsn_obj.encoded_string()).
394
+ schemadiff_as_sql(results.db(self.target_dsn_obj.encoded_string())))
395
+
396
+ schema = self._extract_schema()
397
+
398
+ if diff:
399
+ self._save_schema_diff(schema, diff)
400
+ else:
401
+ logger.info("No changes made for %s", self.migration_name)
402
+
403
+ with main_database_requester.get_connection() as connection:
404
+ with connection.cursor() as cursor:
405
+ cursor.execute(
406
+ """
407
+ SELECT pg_terminate_backend(pid)
408
+ FROM pg_stat_activity
409
+ WHERE datname = %s
410
+ AND pid <> pg_backend_pid();
411
+ """, (self.test_dsn_obj.path.lstrip('/'),)
412
+ )
413
+ cursor.execute(
414
+ sql.SQL(
415
+ """
416
+ DROP
417
+ DATABASE IF EXISTS {};
418
+ """
419
+ ).format(sql.Identifier(self.test_dsn_obj.path.lstrip('/')))
420
+ )
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: psql_vcs
3
+ Version: 0.0.1a0
4
+ Summary: Postgres version control system
5
+ Author-email: Beloborod <me@beloborod.ru>
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: results>=2.0.1783086339
18
+ Requires-Dist: psycopg-binary>=3.3.4
19
+ Requires-Dist: psycopg>=3.3.4
20
+ Requires-Dist: pydantic>=2.13.4
21
+ Requires-Dist: psycopg-pool>=3.3.1
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/psql_vcs/__init__.py
4
+ src/psql_vcs.egg-info/PKG-INFO
5
+ src/psql_vcs.egg-info/SOURCES.txt
6
+ src/psql_vcs.egg-info/dependency_links.txt
7
+ src/psql_vcs.egg-info/requires.txt
8
+ src/psql_vcs.egg-info/top_level.txt
9
+ src/psql_vcs/models/__init__.py
10
+ src/psql_vcs/models/migrator_args.py
11
+ src/psql_vcs/models/schemas.py
12
+ src/psql_vcs/modules/__init__.py
13
+ src/psql_vcs/modules/connector.py
14
+ src/psql_vcs/modules/logger.py
15
+ src/psql_vcs/modules/postgres_schema_processing.py
@@ -0,0 +1,5 @@
1
+ results>=2.0.1783086339
2
+ psycopg-binary>=3.3.4
3
+ psycopg>=3.3.4
4
+ pydantic>=2.13.4
5
+ psycopg-pool>=3.3.1
@@ -0,0 +1 @@
1
+ psql_vcs