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.
@@ -1,14 +1,27 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: postgresledger
3
- Version: 0.0.1
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: Environment :: Console
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
+ [![PyPI](https://img.shields.io/pypi/v/postgresledger)](https://pypi.org/project/postgresledger/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/postgresledger)](https://pypi.org/project/postgresledger/)
37
+ [![Tests](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml/badge.svg)](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml)
38
+ [![License: MIT](https://img.shields.io/pypi/l/postgresledger)](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
+ [![PyPI](https://img.shields.io/pypi/v/postgresledger)](https://pypi.org/project/postgresledger/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/postgresledger)](https://pypi.org/project/postgresledger/)
5
+ [![Tests](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml/badge.svg)](https://github.com/nmrkic/postgresledger/actions/workflows/tests.yml)
6
+ [![License: MIT](https://img.shields.io/pypi/l/postgresledger)](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.1"
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
- "Environment :: Console",
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.1" # set from the release tag by .github/workflows/publish.yml, not here
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
- "Environment :: Console",
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
- print(plan.human)
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
- print(plan.human)
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) -> Migration | None:
269
- """schema.sql vs (every migration applied) -> new migration, then check that it reproduces schema.sql."""
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
- print(plan.human)
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