half-orm-dev 1.0.0a33__tar.gz → 1.0.0a35__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.
- {half_orm_dev-1.0.0a33/half_orm_dev.egg-info → half_orm_dev-1.0.0a35}/PKG-INFO +2 -2
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/release.py +2 -2
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/database.py +7 -1
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/file_executor.py +4 -2
- half_orm_dev-1.0.0a35/half_orm_dev/migrations/1/0/0/a35/00_drop_stale_release_schemas.py +145 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/release_manager.py +1 -1
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/repo.py +69 -19
- half_orm_dev-1.0.0a35/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35/half_orm_dev.egg-info}/PKG-INFO +2 -2
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/SOURCES.txt +1 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/requires.txt +1 -1
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/setup.py +1 -1
- half_orm_dev-1.0.0a33/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/LICENSE +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/README.md +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/_bootstrap_runner.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/recover.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/revert_migration.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/rollback.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/main.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/hgit.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migration_manager.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/1/0/0/a33/00_metadata_to_data_sql.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patch_manager.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/py.typed +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/pre-push +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/venv_setup.py +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/entry_points.txt +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/tests/test_bootstrap_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: half_orm_dev
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a35
|
|
4
4
|
Summary: half_orm development Framework.
|
|
5
5
|
Author-email: Joël Maïzi <joel.maizi@collorg.org>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -25,7 +25,7 @@ Requires-Dist: packaging
|
|
|
25
25
|
Requires-Dist: pydash
|
|
26
26
|
Requires-Dist: pytest
|
|
27
27
|
Requires-Dist: pytest-asyncio
|
|
28
|
-
Requires-Dist: half_orm<1.1.0,>=1.0.
|
|
28
|
+
Requires-Dist: half_orm<1.1.0,>=1.0.0
|
|
29
29
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
30
30
|
Requires-Dist: tomli_w>=1.0.0
|
|
31
31
|
Dynamic: license-file
|
|
@@ -180,7 +180,7 @@ def release_promote(target: str) -> None:
|
|
|
180
180
|
2. Validate sequential version rule
|
|
181
181
|
3. Acquire distributed lock on ho-prod
|
|
182
182
|
4. Restore database and apply all patches
|
|
183
|
-
5. Generate schema-X.Y.Z.sql and
|
|
183
|
+
5. Generate schema-X.Y.Z.sql and data-X.Y.Z.sql
|
|
184
184
|
6. Update schema.sql symlink
|
|
185
185
|
7. Rename RC file to production file (git mv)
|
|
186
186
|
8. Commit and push promotion
|
|
@@ -203,7 +203,7 @@ def release_promote(target: str) -> None:
|
|
|
203
203
|
Output:
|
|
204
204
|
✓ Promoted 1.3.5-rc1 → 1.3.5
|
|
205
205
|
✓ Generated schema-1.3.5.sql
|
|
206
|
-
✓ Generated
|
|
206
|
+
✓ Generated data-1.3.5.sql
|
|
207
207
|
✓ Updated schema.sql → schema-1.3.5.sql
|
|
208
208
|
|
|
209
209
|
\b
|
|
@@ -398,7 +398,11 @@ class Database:
|
|
|
398
398
|
)
|
|
399
399
|
|
|
400
400
|
# Filter to keep only COPY blocks (COPY ... FROM stdin; ... \.)
|
|
401
|
-
#
|
|
401
|
+
# and the SELECT pg_catalog.setval(...) calls pg_dump emits
|
|
402
|
+
# after them - dropping those would leave every serial/identity
|
|
403
|
+
# sequence at its default start value, causing the next INSERT
|
|
404
|
+
# relying on it to collide with rows restored by COPY. Version-
|
|
405
|
+
# specific SET commands are what we actually want to avoid.
|
|
402
406
|
content = temp_file.read_text()
|
|
403
407
|
filtered_lines = []
|
|
404
408
|
in_copy_block = False
|
|
@@ -410,6 +414,8 @@ class Database:
|
|
|
410
414
|
if line == '\\.':
|
|
411
415
|
in_copy_block = False
|
|
412
416
|
filtered_lines.append('') # Empty line between blocks
|
|
417
|
+
elif not in_copy_block and line.startswith('SELECT pg_catalog.setval('):
|
|
418
|
+
filtered_lines.append(line)
|
|
413
419
|
|
|
414
420
|
data_file.write_text('\n'.join(filtered_lines))
|
|
415
421
|
except Exception as e:
|
|
@@ -59,9 +59,11 @@ def execute_sql_file_psql(file_path: Path, database, database_name: str) -> None
|
|
|
59
59
|
FileExecutionError: If psql execution fails
|
|
60
60
|
"""
|
|
61
61
|
try:
|
|
62
|
-
database.execute_pg_command('psql', '-d', database_name, '-f', str(file_path))
|
|
62
|
+
database.execute_pg_command('psql', '-v', 'ON_ERROR_STOP=1', '-d', database_name, '-f', str(file_path))
|
|
63
63
|
except Exception as e:
|
|
64
|
-
|
|
64
|
+
stderr = getattr(e, 'stderr', None)
|
|
65
|
+
detail = f"{e}\n{stderr.strip()}" if stderr else str(e)
|
|
66
|
+
raise FileExecutionError(f"psql execution failed for {file_path.name}: {detail}") from e
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def execute_python_file(
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration 1.0.0a35 — drop stale release-X.Y.Z.sql on active branches
|
|
3
|
+
|
|
4
|
+
Before 1.0.0a33, restore_database_from_schema() rebuilt the database
|
|
5
|
+
from model/schema.sql + model/metadata-{version}.sql, and the latter
|
|
6
|
+
held half_orm_meta bookkeeping *only* - no application data.
|
|
7
|
+
generate_release_schema() then took a full pg_dump of that database, so
|
|
8
|
+
every model/release-{version}.sql produced this way captured the
|
|
9
|
+
structure plus the half_orm_meta rows and nothing else.
|
|
10
|
+
|
|
11
|
+
The a33 migration backfilled model/data-{version}.sql with the real,
|
|
12
|
+
complete snapshot, but left those release files untouched. They are
|
|
13
|
+
still what `patch apply` and `patch merge` restore from whenever a
|
|
14
|
+
release is in flight, so development databases come back up with no
|
|
15
|
+
application data at all - the symptom that motivated this migration
|
|
16
|
+
(a project whose release-0.3.10.sql carried 78 half_orm_meta.hop_release
|
|
17
|
+
rows and zero rows for its 39 other tables, against 2098 rows in
|
|
18
|
+
data-0.3.9.sql).
|
|
19
|
+
|
|
20
|
+
Rather than rebuilding those files here - which would mean restoring a
|
|
21
|
+
database and replaying every staged patch, per branch - this deletes
|
|
22
|
+
them. They are derived artifacts: the fallback already in
|
|
23
|
+
apply_patch_complete_workflow() rebuilds one from
|
|
24
|
+
restore_database_from_schema() (schema + full data) plus the staged
|
|
25
|
+
patches, then calls generate_release_schema() again. Deleting is
|
|
26
|
+
therefore self-healing, and costs one rebuild on the next `patch apply`.
|
|
27
|
+
|
|
28
|
+
A release schema is carried by every active branch: create_release()
|
|
29
|
+
commits it on ho-release/X.Y.Z, patch branches inherit a copy, and .hop/
|
|
30
|
+
syncing propagates it to ho-prod as well. All of them have to be
|
|
31
|
+
cleaned, and ho-prod in particular must not be skipped - syncing copies
|
|
32
|
+
.hop/ with `git checkout <source> -- .hop/`, so a copy left on ho-prod
|
|
33
|
+
would be restored onto every branch this migration just cleaned. For
|
|
34
|
+
the same reason the deletion never propagates on its own: that checkout
|
|
35
|
+
adds and overwrites, but never deletes.
|
|
36
|
+
|
|
37
|
+
Ordering therefore matters. Other branches are handled first, each with
|
|
38
|
+
its own commit, while ho-prod's copy is only staged - the migration
|
|
39
|
+
runner commits it, then pushes ho-prod, and only then syncs .hop/
|
|
40
|
+
outwards.
|
|
41
|
+
|
|
42
|
+
Committing on other branches is covered by the existing safety net:
|
|
43
|
+
capture_branches_snapshot() records ho-prod, release and patch branches
|
|
44
|
+
alike, and rollback_to_snapshot() resets each one should the migration
|
|
45
|
+
fail later. Following the convention in
|
|
46
|
+
Repo.sync_hop_to_active_branches(), those local commits are the
|
|
47
|
+
transaction (a failure propagates and triggers that rollback) while push
|
|
48
|
+
failures are reported rather than raised - the deletion is already
|
|
49
|
+
recorded locally, and an unpushed branch is recoverable.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
import sys
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_description():
|
|
57
|
+
return "Delete release-X.Y.Z.sql files generated before the metadata->data.sql switch"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _affected_branches(repo):
|
|
61
|
+
"""Active release and patch branches, both of which carry release schemas."""
|
|
62
|
+
status = repo.hgit.get_active_branches_status()
|
|
63
|
+
names = []
|
|
64
|
+
for key in ('release_branches', 'patch_branches'):
|
|
65
|
+
for branch in status.get(key) or []:
|
|
66
|
+
name = branch.get('name')
|
|
67
|
+
if name and name not in names:
|
|
68
|
+
names.append(name)
|
|
69
|
+
return names
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _stale_release_schemas(repo, model_dir):
|
|
73
|
+
"""Repo-relative paths of the release schemas present on this branch."""
|
|
74
|
+
return [
|
|
75
|
+
str(path.relative_to(repo.base_dir))
|
|
76
|
+
for path in sorted(model_dir.glob('release-*.sql'))
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def migrate(repo):
|
|
81
|
+
model_dir = Path(repo.model_dir)
|
|
82
|
+
|
|
83
|
+
# Only projects predating the a33 metadata->data switch are affected.
|
|
84
|
+
# A leftover model/metadata-*.sql is the proof: it is what the old
|
|
85
|
+
# restore path loaded, so any release schema this project generated
|
|
86
|
+
# was dumped from a database holding half_orm_meta rows only. A
|
|
87
|
+
# project that never had those files has correct release schemas and
|
|
88
|
+
# must not be forced into a needless rebuild.
|
|
89
|
+
if not list(model_dir.glob('metadata-*.sql')):
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
git_repo = repo.hgit.git_repo
|
|
93
|
+
current_branch = repo.hgit.branch
|
|
94
|
+
deleted = []
|
|
95
|
+
to_push = []
|
|
96
|
+
|
|
97
|
+
# Other active branches first, each committed on the spot: the
|
|
98
|
+
# current branch has to stay clean while switching away from it.
|
|
99
|
+
for branch in _affected_branches(repo):
|
|
100
|
+
if branch == current_branch:
|
|
101
|
+
continue
|
|
102
|
+
with repo.hgit.on_branch(branch, silent=True):
|
|
103
|
+
stale = _stale_release_schemas(repo, model_dir)
|
|
104
|
+
if not stale:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
git_repo.index.remove(stale, working_tree=True)
|
|
108
|
+
git_repo.index.commit(
|
|
109
|
+
'[HOP] Remove stale release schema '
|
|
110
|
+
'(generated before metadata->data.sql; regenerated with '
|
|
111
|
+
'full data on the next patch apply)',
|
|
112
|
+
skip_hooks=True,
|
|
113
|
+
)
|
|
114
|
+
deleted.extend(f"{branch}:{name}" for name in stale)
|
|
115
|
+
to_push.append(branch)
|
|
116
|
+
|
|
117
|
+
# push_branch() names its target explicitly, so it does not depend on
|
|
118
|
+
# the branch currently checked out.
|
|
119
|
+
for branch in to_push:
|
|
120
|
+
try:
|
|
121
|
+
repo.hgit.push_branch(branch)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(
|
|
124
|
+
f" ⚠ Could not push {branch}: {e}\n"
|
|
125
|
+
f" The stale release schema was removed locally; "
|
|
126
|
+
f"push {branch} manually to share the change.",
|
|
127
|
+
file=sys.stderr,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ho-prod last, and removed from the working tree only. The migration
|
|
131
|
+
# runner always stages '.hop/' before committing, and `git add` on a
|
|
132
|
+
# directory records deletions inside it, so the removal lands in the
|
|
133
|
+
# migration commit - which is pushed before .hop/ is synced outwards.
|
|
134
|
+
# Skipping ho-prod would leave a copy here that the sync copies
|
|
135
|
+
# straight back onto every branch cleaned above.
|
|
136
|
+
#
|
|
137
|
+
# Staging it here instead would break that runner: it feeds every
|
|
138
|
+
# path it finds staged to `git add`, and `git add` on an
|
|
139
|
+
# already-staged deletion fails with "did not match any files".
|
|
140
|
+
stale_here = _stale_release_schemas(repo, model_dir)
|
|
141
|
+
for relative in stale_here:
|
|
142
|
+
(Path(repo.base_dir) / relative).unlink()
|
|
143
|
+
deleted.append(f"{current_branch}:{relative}")
|
|
144
|
+
|
|
145
|
+
return {'deleted_release_schemas': deleted}
|
|
@@ -306,7 +306,7 @@ class ReleaseManager:
|
|
|
306
306
|
try:
|
|
307
307
|
self._repo.database._generate_schema_sql(version_str, model_dir)
|
|
308
308
|
click.echo(f" ✓ Generated schema-{version_str}.sql")
|
|
309
|
-
click.echo(f" ✓ Generated
|
|
309
|
+
click.echo(f" ✓ Generated data-{version_str}.sql")
|
|
310
310
|
click.echo(f" ✓ Created symlink: schema.sql -> schema-{version_str}.sql")
|
|
311
311
|
except Exception as e:
|
|
312
312
|
raise ReleaseManagerError(f"Failed to generate schema files: {e}")
|
|
@@ -66,6 +66,20 @@ def _git_origin_to_https(git_origin: str) -> str:
|
|
|
66
66
|
return url
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _pg_command_error_detail(e: Exception) -> str:
|
|
70
|
+
"""Format a psql/pg_dump failure with its actual stderr, when available.
|
|
71
|
+
|
|
72
|
+
str(subprocess.CalledProcessError) only shows the command and exit
|
|
73
|
+
code - the actual SQL error psql printed to stderr (e.g. which
|
|
74
|
+
statement failed and why) is on e.stderr and would otherwise be
|
|
75
|
+
silently dropped from every RepoError raised around a restore.
|
|
76
|
+
"""
|
|
77
|
+
stderr = getattr(e, 'stderr', None)
|
|
78
|
+
if stderr:
|
|
79
|
+
return f"{e}\n{stderr.strip() if isinstance(stderr, str) else stderr}"
|
|
80
|
+
return str(e)
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
class RepoError(Exception):
|
|
70
84
|
pass
|
|
71
85
|
|
|
@@ -2362,11 +2376,22 @@ Files are executed **alphabetically** (not numerically parsed).
|
|
|
2362
2376
|
|
|
2363
2377
|
## Execution Context
|
|
2364
2378
|
|
|
2365
|
-
Bootstrap scripts run
|
|
2366
|
-
|
|
2367
|
-
-
|
|
2379
|
+
Bootstrap scripts run **once**: when a brand-new instance is created via
|
|
2380
|
+
`clone`. They do NOT run on `patch apply`, `patch merge`, or
|
|
2381
|
+
`release promote` - there is no database reset between those, so a
|
|
2382
|
+
script that isn't independently idempotent would corrupt the database if
|
|
2383
|
+
it ran more than once.
|
|
2368
2384
|
|
|
2369
|
-
|
|
2385
|
+
Reserve `bootstrap/` for data that is genuinely specific to *this*
|
|
2386
|
+
instance (e.g. an initial admin account, a per-site secret) - never for
|
|
2387
|
+
reference/system data that every instance should have.
|
|
2388
|
+
|
|
2389
|
+
Reference/system data that every instance should ship with (e.g. a new
|
|
2390
|
+
system role) does NOT belong in `bootstrap/`: write it as ordinary
|
|
2391
|
+
idempotent DML in a **patch** instead. It reaches existing instances
|
|
2392
|
+
through the normal `hop upgrade` path, and gets captured automatically
|
|
2393
|
+
in `model/data-X.Y.Z.sql` (the schema+data snapshot restored by every
|
|
2394
|
+
`patch apply`) once shipped - `bootstrap/` scripts never are.
|
|
2370
2395
|
|
|
2371
2396
|
## Python Files
|
|
2372
2397
|
|
|
@@ -2637,9 +2662,21 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2637
2662
|
and should persist across schema resets.
|
|
2638
2663
|
"""
|
|
2639
2664
|
schemas_to_drop = {'half_orm_meta', 'half_orm_meta.view'}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2665
|
+
|
|
2666
|
+
# Enumerate schemas from the PostgreSQL catalog rather than from
|
|
2667
|
+
# self.model.desc(), which only lists *relations*: a schema holding
|
|
2668
|
+
# no table/view (only functions, types, sequences - or nothing at
|
|
2669
|
+
# all) is invisible there, so it would survive the reset and then
|
|
2670
|
+
# collide with the CREATE SCHEMA emitted by the file being
|
|
2671
|
+
# restored ("schema already exists"), leaving the database in a
|
|
2672
|
+
# state that no longer matches the dump.
|
|
2673
|
+
cursor = self.model.execute_query(
|
|
2674
|
+
"SELECT nspname FROM pg_catalog.pg_namespace "
|
|
2675
|
+
"WHERE nspname NOT IN ('pg_catalog', 'information_schema') "
|
|
2676
|
+
"AND nspname NOT LIKE 'pg\\_%'"
|
|
2677
|
+
)
|
|
2678
|
+
for row in cursor:
|
|
2679
|
+
schemas_to_drop.add(row['nspname'])
|
|
2643
2680
|
|
|
2644
2681
|
# Drop each schema with CASCADE
|
|
2645
2682
|
for schema_name in schemas_to_drop:
|
|
@@ -2733,10 +2770,13 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2733
2770
|
# 3. Load schema from model/schema.sql
|
|
2734
2771
|
try:
|
|
2735
2772
|
self.database.execute_pg_command(
|
|
2736
|
-
'psql', '-d', self.database_name, '-f', str(schema_path)
|
|
2773
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(schema_path)
|
|
2737
2774
|
)
|
|
2738
2775
|
except Exception as e:
|
|
2739
|
-
raise RepoError(
|
|
2776
|
+
raise RepoError(
|
|
2777
|
+
f"Failed to load schema from {schema_path.name}: "
|
|
2778
|
+
f"{_pg_command_error_detail(e)}"
|
|
2779
|
+
) from e
|
|
2740
2780
|
|
|
2741
2781
|
# 4. Load data from model/data-X.Y.Z.sql (if exists)
|
|
2742
2782
|
data_path, version = self._deduce_data_path(schema_path)
|
|
@@ -2744,11 +2784,12 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2744
2784
|
if data_path and data_path.exists():
|
|
2745
2785
|
try:
|
|
2746
2786
|
self.database.execute_pg_command(
|
|
2747
|
-
'psql', '-d', self.database_name, '-f', str(data_path)
|
|
2787
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(data_path)
|
|
2748
2788
|
)
|
|
2749
2789
|
except Exception as e:
|
|
2750
2790
|
raise RepoError(
|
|
2751
|
-
f"Failed to load data from {data_path.name}:
|
|
2791
|
+
f"Failed to load data from {data_path.name}: "
|
|
2792
|
+
f"{_pg_command_error_detail(e)}"
|
|
2752
2793
|
) from e
|
|
2753
2794
|
# else: data file doesn't exist, continue without error (backward compatibility)
|
|
2754
2795
|
|
|
@@ -2796,20 +2837,24 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2796
2837
|
|
|
2797
2838
|
try:
|
|
2798
2839
|
self.database.execute_pg_command(
|
|
2799
|
-
'psql', '-d', self.database_name, '-f', str(schema_path)
|
|
2840
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(schema_path)
|
|
2800
2841
|
)
|
|
2801
2842
|
except Exception as e:
|
|
2802
|
-
raise RepoError(
|
|
2843
|
+
raise RepoError(
|
|
2844
|
+
f"Failed to load schema from {schema_path.name}: "
|
|
2845
|
+
f"{_pg_command_error_detail(e)}"
|
|
2846
|
+
) from e
|
|
2803
2847
|
|
|
2804
2848
|
data_path = Path(self.model_dir) / f"data-{version}.sql"
|
|
2805
2849
|
if data_path.exists():
|
|
2806
2850
|
try:
|
|
2807
2851
|
self.database.execute_pg_command(
|
|
2808
|
-
'psql', '-d', self.database_name, '-f', str(data_path)
|
|
2852
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(data_path)
|
|
2809
2853
|
)
|
|
2810
2854
|
except Exception as e:
|
|
2811
2855
|
raise RepoError(
|
|
2812
|
-
f"Failed to load data from {data_path.name}:
|
|
2856
|
+
f"Failed to load data from {data_path.name}: "
|
|
2857
|
+
f"{_pg_command_error_detail(e)}"
|
|
2813
2858
|
) from e
|
|
2814
2859
|
|
|
2815
2860
|
self.model.reconnect(reload=True)
|
|
@@ -2862,10 +2907,13 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2862
2907
|
# 2. Load dump using psql
|
|
2863
2908
|
try:
|
|
2864
2909
|
self.database.execute_pg_command(
|
|
2865
|
-
'psql', '-d', self.database_name, '-f', str(dump_path)
|
|
2910
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(dump_path)
|
|
2866
2911
|
)
|
|
2867
2912
|
except Exception as e:
|
|
2868
|
-
raise RepoError(
|
|
2913
|
+
raise RepoError(
|
|
2914
|
+
f"Failed to load dump from {dump_path.name}: "
|
|
2915
|
+
f"{_pg_command_error_detail(e)}"
|
|
2916
|
+
) from e
|
|
2869
2917
|
|
|
2870
2918
|
# 3. Reload half_orm metadata cache
|
|
2871
2919
|
self.model.reconnect(reload=True)
|
|
@@ -2987,14 +3035,16 @@ INSERT INTO public.roles (name) VALUES ('admin'), ('user');
|
|
|
2987
3035
|
|
|
2988
3036
|
# Load release schema
|
|
2989
3037
|
self.database.execute_pg_command(
|
|
2990
|
-
'psql', '-d', self.database_name, '-f', str(release_schema_path)
|
|
3038
|
+
'psql', '-v', 'ON_ERROR_STOP=1', '-d', self.database_name, '-f', str(release_schema_path)
|
|
2991
3039
|
)
|
|
2992
3040
|
|
|
2993
3041
|
# Reload half_orm metadata cache
|
|
2994
3042
|
self.model.reconnect(reload=True)
|
|
2995
3043
|
|
|
2996
3044
|
except Exception as e:
|
|
2997
|
-
raise RepoError(
|
|
3045
|
+
raise RepoError(
|
|
3046
|
+
f"Failed to restore from release schema: {_pg_command_error_detail(e)}"
|
|
3047
|
+
) from e
|
|
2998
3048
|
|
|
2999
3049
|
def get_release_schema_path(self, version: str) -> Path:
|
|
3000
3050
|
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0-a35
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: half_orm_dev
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a35
|
|
4
4
|
Summary: half_orm development Framework.
|
|
5
5
|
Author-email: Joël Maïzi <joel.maizi@collorg.org>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -25,7 +25,7 @@ Requires-Dist: packaging
|
|
|
25
25
|
Requires-Dist: pydash
|
|
26
26
|
Requires-Dist: pytest
|
|
27
27
|
Requires-Dist: pytest-asyncio
|
|
28
|
-
Requires-Dist: half_orm<1.1.0,>=1.0.
|
|
28
|
+
Requires-Dist: half_orm<1.1.0,>=1.0.0
|
|
29
29
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
30
30
|
Requires-Dist: tomli_w>=1.0.0
|
|
31
31
|
Dynamic: license-file
|
|
@@ -55,6 +55,7 @@ half_orm_dev/migrations/0/18/0/00_add_async_support.py
|
|
|
55
55
|
half_orm_dev/migrations/0/18/0/01_update_default_tests.py
|
|
56
56
|
half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py
|
|
57
57
|
half_orm_dev/migrations/1/0/0/a33/00_metadata_to_data_sql.py
|
|
58
|
+
half_orm_dev/migrations/1/0/0/a35/00_drop_stale_release_schemas.py
|
|
58
59
|
half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md
|
|
59
60
|
half_orm_dev/patches/log
|
|
60
61
|
half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql
|
|
@@ -17,7 +17,7 @@ from setuptools import setup
|
|
|
17
17
|
# for half_orm_dev 0.17.x where 3 <= x < 5, require half_orm >= 0.17.1
|
|
18
18
|
HALF_ORM_MIN_VERSIONS = [
|
|
19
19
|
(0, 17, 3, None, '0.17.3'), # 0.17.3+ requires half_orm >= 0.17.3 (CustomGroup support)
|
|
20
|
-
(1, 0, 0, None, '1.0.
|
|
20
|
+
(1, 0, 0, None, '1.0.0'), # 1.0.0+ requires half_orm >= 1.0.0 (stable release)
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0-a33
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/revert_migration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py
RENAMED
|
File without changes
|
{half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/prepare-commit-msg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|