postgresledger 0.0.1__tar.gz → 0.0.2__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.
- {postgresledger-0.0.1 → postgresledger-0.0.2}/PKG-INFO +22 -2
- {postgresledger-0.0.1 → postgresledger-0.0.2}/README.md +7 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/pyproject.toml +24 -2
- {postgresledger-0.0.1 → postgresledger-0.0.2}/pyproject.toml.orig +15 -2
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/cli.py +9 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/commands.py +7 -5
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/core.py +31 -5
- {postgresledger-0.0.1 → postgresledger-0.0.2}/LICENSE +0 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/__init__.py +0 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/compare.py +0 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/config.py +0 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/pgschema.py +0 -0
- {postgresledger-0.0.1 → postgresledger-0.0.2}/src/postgresledger/scratch.py +0 -0
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: postgresledger
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Versioned PostgreSQL migrations from plain SQL, without an ORM: generated from schema.sql, tracked per database, with drift detection
|
|
5
|
+
Keywords: postgresql,postgres,migrations,database-migrations,schema,sql,drift,pgschema
|
|
5
6
|
Author: Nebojša Mrkić
|
|
6
7
|
Author-email: Nebojša Mrkić <mrkic.nebojsa@gmail.com>
|
|
7
8
|
License-Expression: MIT
|
|
8
9
|
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
9
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: SQL
|
|
10
23
|
Classifier: Topic :: Database
|
|
11
|
-
Classifier:
|
|
24
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
12
25
|
Requires-Dist: python-dotenv>=1.2.4
|
|
13
26
|
Requires-Python: >=3.11
|
|
14
27
|
Project-URL: Homepage, https://github.com/nmrkic/postgresledger
|
|
@@ -19,6 +32,11 @@ Description-Content-Type: text/markdown
|
|
|
19
32
|
|
|
20
33
|
# postgresledger
|
|
21
34
|
|
|
35
|
+
[](https://pypi.org/project/postgresledger/)
|
|
36
|
+
[](https://pypi.org/project/postgresledger/)
|
|
37
|
+
[](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml)
|
|
38
|
+
[](https://github.com/nmrkic/postgresledger/blob/main/LICENSE)
|
|
39
|
+
|
|
22
40
|
Versioned migrations for PostgreSQL from plain SQL, without an ORM (tables,
|
|
23
41
|
views, materialized views, functions, triggers), built on
|
|
24
42
|
[pgschema](https://www.pgschema.com). The command is `pgl`.
|
|
@@ -264,6 +282,8 @@ environment; a variable set there wins over `.env`. `pgl --env-file .env.uat …
|
|
|
264
282
|
(or `PGL_ENV_FILE=.env.uat`) picks another file for one run.
|
|
265
283
|
Run `pgl` in the folder with `postgresledger.toml`, or point to it with
|
|
266
284
|
`--config PATH`. `pgl <command> -h` describes each command.
|
|
285
|
+
`baseline`, `new`, `capture` and `drift` print only what changed;
|
|
286
|
+
`--verbose` (`-v`) prints pgschema's whole plan, SQL included.
|
|
267
287
|
|
|
268
288
|
## Developing postgresledger
|
|
269
289
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# postgresledger
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/postgresledger/)
|
|
4
|
+
[](https://pypi.org/project/postgresledger/)
|
|
5
|
+
[](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml)
|
|
6
|
+
[](https://github.com/nmrkic/postgresledger/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
Versioned migrations for PostgreSQL from plain SQL, without an ORM (tables,
|
|
4
9
|
views, materialized views, functions, triggers), built on
|
|
5
10
|
[pgschema](https://www.pgschema.com). The command is `pgl`.
|
|
@@ -245,6 +250,8 @@ environment; a variable set there wins over `.env`. `pgl --env-file .env.uat …
|
|
|
245
250
|
(or `PGL_ENV_FILE=.env.uat`) picks another file for one run.
|
|
246
251
|
Run `pgl` in the folder with `postgresledger.toml`, or point to it with
|
|
247
252
|
`--config PATH`. `pgl <command> -h` describes each command.
|
|
253
|
+
`baseline`, `new`, `capture` and `drift` print only what changed;
|
|
254
|
+
`--verbose` (`-v`) prints pgschema's whole plan, SQL included.
|
|
248
255
|
|
|
249
256
|
## Developing postgresledger
|
|
250
257
|
|
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "postgresledger"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
description = "Versioned PostgreSQL migrations from plain SQL, without an ORM: generated from schema.sql, tracked per database, with drift detection"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
requires-python = ">=3.11"
|
|
9
9
|
dependencies = ["python-dotenv>=1.2.4"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"postgresql",
|
|
12
|
+
"postgres",
|
|
13
|
+
"migrations",
|
|
14
|
+
"database-migrations",
|
|
15
|
+
"schema",
|
|
16
|
+
"sql",
|
|
17
|
+
"drift",
|
|
18
|
+
"pgschema",
|
|
19
|
+
]
|
|
10
20
|
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: System Administrators",
|
|
25
|
+
"Operating System :: POSIX :: Linux",
|
|
26
|
+
"Operating System :: MacOS",
|
|
11
27
|
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Programming Language :: SQL",
|
|
12
34
|
"Topic :: Database",
|
|
13
|
-
"
|
|
35
|
+
"Topic :: Software Development :: Version Control",
|
|
14
36
|
]
|
|
15
37
|
|
|
16
38
|
[[project.authors]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "postgresledger"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2" # set from the release tag by .github/workflows/publish.yml, not here
|
|
4
4
|
description = "Versioned PostgreSQL migrations from plain SQL, without an ORM: generated from schema.sql, tracked per database, with drift detection"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -10,10 +10,23 @@ authors = [{ name = "Nebojša Mrkić", email = "mrkic.nebojsa@gmail.com" }]
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"python-dotenv>=1.2.4",
|
|
12
12
|
]
|
|
13
|
+
keywords = ["postgresql", "postgres", "migrations", "database-migrations", "schema", "sql", "drift", "pgschema"]
|
|
13
14
|
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: MacOS",
|
|
14
21
|
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: SQL",
|
|
15
28
|
"Topic :: Database",
|
|
16
|
-
"
|
|
29
|
+
"Topic :: Software Development :: Version Control",
|
|
17
30
|
]
|
|
18
31
|
|
|
19
32
|
[project.urls]
|
|
@@ -17,6 +17,8 @@ NO_CONFIG = {"init", "setup"}
|
|
|
17
17
|
# commands that read the project but never touch a database
|
|
18
18
|
# (compare with two snapshots; compare with one checks DATABASE_NAME itself)
|
|
19
19
|
NO_DATABASE = {"new", "compare"}
|
|
20
|
+
# commands that make a pgschema plan; --verbose prints all of it, SQL included
|
|
21
|
+
VERBOSE = {"baseline", "new", "drift", "capture"}
|
|
20
22
|
|
|
21
23
|
INTRO = """\
|
|
22
24
|
Versioned PostgreSQL migrations from plain SQL, without an ORM.
|
|
@@ -60,6 +62,11 @@ def add_apply_arguments(command_parser: argparse.ArgumentParser) -> None:
|
|
|
60
62
|
command_parser.add_argument("--dry-run", action="store_true", help="only list what would be applied")
|
|
61
63
|
|
|
62
64
|
|
|
65
|
+
def add_verbose_argument(command_parser: argparse.ArgumentParser) -> None:
|
|
66
|
+
command_parser.add_argument("-v", "--verbose", action="store_true",
|
|
67
|
+
help="print pgschema's whole plan, including the SQL")
|
|
68
|
+
|
|
69
|
+
|
|
63
70
|
def add_snapshot_arguments(command_parser: argparse.ArgumentParser) -> None:
|
|
64
71
|
command_parser.add_argument("name", help="label for the database, e.g. prod")
|
|
65
72
|
|
|
@@ -180,6 +187,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
180
187
|
command_parser.set_defaults(fn=command_function)
|
|
181
188
|
if command.add_arguments is not None:
|
|
182
189
|
command.add_arguments(command_parser)
|
|
190
|
+
if command.name in VERBOSE:
|
|
191
|
+
add_verbose_argument(command_parser)
|
|
183
192
|
return main_parser
|
|
184
193
|
|
|
185
194
|
|
|
@@ -19,6 +19,7 @@ from .core import (
|
|
|
19
19
|
list_migrations,
|
|
20
20
|
pgschema_dump,
|
|
21
21
|
pgschema_plan,
|
|
22
|
+
print_plan,
|
|
22
23
|
psql,
|
|
23
24
|
record_migration_sql,
|
|
24
25
|
require_up_to_date,
|
|
@@ -112,7 +113,8 @@ def cmd_baseline(cfg: Config, connection: Connection, args) -> int:
|
|
|
112
113
|
cfg.schema_file.parent.mkdir(parents=True, exist_ok=True)
|
|
113
114
|
print(f"- dump {connection.label} -> {relative(cfg.schema_file)}")
|
|
114
115
|
pgschema_dump(cfg, connection, cfg.schema_file)
|
|
115
|
-
baseline = generate_migration(cfg, scratch_server, "baseline", f"baseline taken from {connection.database}"
|
|
116
|
+
baseline = generate_migration(cfg, scratch_server, "baseline", f"baseline taken from {connection.database}",
|
|
117
|
+
args.verbose)
|
|
116
118
|
if baseline is None:
|
|
117
119
|
sys.exit("the database is empty, nothing to snapshot")
|
|
118
120
|
ensure_meta(cfg, connection)
|
|
@@ -130,7 +132,7 @@ def cmd_baseline(cfg: Config, connection: Connection, args) -> int:
|
|
|
130
132
|
plan = pgschema_plan(cfg, shadow, actual, scratch_server)
|
|
131
133
|
if plan.sql:
|
|
132
134
|
print(f"✘ {connection.label} does not match the migrations, so they cannot be recorded as already there:\n")
|
|
133
|
-
|
|
135
|
+
print_plan(plan, args.verbose)
|
|
134
136
|
print("\nempty database: `pgl apply` instead; otherwise align it first (see `pgl compare`)")
|
|
135
137
|
return 1
|
|
136
138
|
# every record in one transaction: all of them or none
|
|
@@ -142,7 +144,7 @@ def cmd_baseline(cfg: Config, connection: Connection, args) -> int:
|
|
|
142
144
|
|
|
143
145
|
def cmd_new(cfg: Config, args) -> None:
|
|
144
146
|
with scratch.server() as scratch_server:
|
|
145
|
-
generate_migration(cfg, scratch_server, args.name, args.note or "change in schema.sql")
|
|
147
|
+
generate_migration(cfg, scratch_server, args.name, args.note or "change in schema.sql", args.verbose)
|
|
146
148
|
|
|
147
149
|
|
|
148
150
|
def cmd_apply(cfg: Config, connection: Connection, args) -> None:
|
|
@@ -217,7 +219,7 @@ def cmd_drift(cfg: Config, connection: Connection, args) -> int:
|
|
|
217
219
|
print(f"✔ {connection.label}: no drift, the database matches its migrations")
|
|
218
220
|
return 0
|
|
219
221
|
print(f"✘ {connection.label}: DRIFT - the database was changed outside of migrations:\n")
|
|
220
|
-
|
|
222
|
+
print_plan(plan, args.verbose)
|
|
221
223
|
if pending:
|
|
222
224
|
print("\nrun `pgl apply` first, then keep or drop the changes:")
|
|
223
225
|
print("\nkeep the changes: pgl capture <name>")
|
|
@@ -233,7 +235,7 @@ def cmd_capture(cfg: Config, connection: Connection, args) -> None:
|
|
|
233
235
|
pgschema_dump(cfg, connection, cfg.schema_file)
|
|
234
236
|
note = f"manual changes captured from {connection.database} (recorded as captured there)"
|
|
235
237
|
with scratch.server() as scratch_server:
|
|
236
|
-
migration = generate_migration(cfg, scratch_server, args.name, note)
|
|
238
|
+
migration = generate_migration(cfg, scratch_server, args.name, note, args.verbose)
|
|
237
239
|
if migration is None:
|
|
238
240
|
cfg.schema_file.write_text(schema_before)
|
|
239
241
|
print(f"{connection.label}: no drift, nothing captured")
|
|
@@ -212,12 +212,32 @@ def pgschema_dump(cfg: Config, connection: Connection, output_file: Path) -> Non
|
|
|
212
212
|
output_file.write_text(dump)
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
# where pgschema's readable plan starts repeating the whole SQL
|
|
216
|
+
DDL_HEADER = "DDL to be executed:"
|
|
217
|
+
|
|
218
|
+
|
|
215
219
|
@dataclass(frozen=True)
|
|
216
220
|
class Plan:
|
|
217
221
|
"""What pgschema would change, in two forms. Both are empty when there is nothing to change."""
|
|
218
222
|
|
|
219
223
|
sql: str # the statements, e.g. for a migration file
|
|
220
|
-
human: str # the same changes as a readable list for the terminal
|
|
224
|
+
human: str # the same changes as a readable list for the terminal, followed by the SQL
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def changes(self) -> str:
|
|
228
|
+
"""`human` without the SQL: the summary and the list of changed objects."""
|
|
229
|
+
return self.human.split(DDL_HEADER)[0].strip()
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def headline(self) -> str:
|
|
233
|
+
"""The size of the plan, e.g. "52 to add" from pgschema's first line "Plan: 52 to add."."""
|
|
234
|
+
first_line = self.human.splitlines()[0] if self.human else ""
|
|
235
|
+
return first_line.removeprefix("Plan:").strip().removesuffix(".")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def print_plan(plan: Plan, verbose: bool) -> None:
|
|
239
|
+
"""The changed objects, and with --verbose also the SQL."""
|
|
240
|
+
print(plan.human if verbose else plan.changes)
|
|
221
241
|
|
|
222
242
|
|
|
223
243
|
def pgschema_plan(cfg: Config, connection: Connection, desired: Path, scratch: Connection) -> Plan:
|
|
@@ -265,8 +285,12 @@ def write_migration(cfg: Config, name: str, sql: str, note: str) -> Migration:
|
|
|
265
285
|
return Migration(version, name, path)
|
|
266
286
|
|
|
267
287
|
|
|
268
|
-
def generate_migration(cfg: Config, scratch: Connection, name: str, note: str
|
|
269
|
-
|
|
288
|
+
def generate_migration(cfg: Config, scratch: Connection, name: str, note: str,
|
|
289
|
+
verbose: bool = False) -> Migration | None:
|
|
290
|
+
"""schema.sql vs (every migration applied) -> new migration, then check that it reproduces schema.sql.
|
|
291
|
+
|
|
292
|
+
The plan is only printed with `verbose`: it is the content of the new file anyway.
|
|
293
|
+
"""
|
|
270
294
|
existing = list_migrations(cfg)
|
|
271
295
|
print(f"- building the state of {len(existing)} existing migrations (scratch Postgres)")
|
|
272
296
|
shadow = build_shadow(cfg, scratch, existing)
|
|
@@ -275,14 +299,16 @@ def generate_migration(cfg: Config, scratch: Connection, name: str, note: str) -
|
|
|
275
299
|
print("no changes compared to the existing migrations")
|
|
276
300
|
return None
|
|
277
301
|
migration = write_migration(cfg, name, plan.sql, note)
|
|
278
|
-
|
|
302
|
+
if verbose:
|
|
303
|
+
print(plan.human)
|
|
279
304
|
print(f"- verifying {migration.path.name}")
|
|
280
305
|
apply_file(cfg, shadow, migration)
|
|
281
306
|
# with the new migration applied, nothing should be left to change
|
|
282
307
|
leftover = pgschema_plan(cfg, shadow, cfg.schema_file, scratch)
|
|
283
308
|
if leftover.sql:
|
|
309
|
+
# always in full: this is the case to debug
|
|
284
310
|
print(leftover.human)
|
|
285
311
|
migration.path.unlink()
|
|
286
312
|
sys.exit("after applying the migration the result still differs from schema.sql - migration deleted")
|
|
287
|
-
print(f"✔ created {relative(migration.path)}")
|
|
313
|
+
print(f"✔ created {relative(migration.path)} ({plan.headline})")
|
|
288
314
|
return migration
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|