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.
Files changed (85) hide show
  1. {half_orm_dev-1.0.0a33/half_orm_dev.egg-info → half_orm_dev-1.0.0a35}/PKG-INFO +2 -2
  2. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/release.py +2 -2
  3. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/database.py +7 -1
  4. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/file_executor.py +4 -2
  5. half_orm_dev-1.0.0a35/half_orm_dev/migrations/1/0/0/a35/00_drop_stale_release_schemas.py +145 -0
  6. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/release_manager.py +1 -1
  7. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/repo.py +69 -19
  8. half_orm_dev-1.0.0a35/half_orm_dev/version.txt +1 -0
  9. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35/half_orm_dev.egg-info}/PKG-INFO +2 -2
  10. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  11. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/requires.txt +1 -1
  12. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/setup.py +1 -1
  13. half_orm_dev-1.0.0a33/half_orm_dev/version.txt +0 -1
  14. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/AUTHORS +0 -0
  15. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/LICENSE +0 -0
  16. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/README.md +0 -0
  17. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/__init__.py +0 -0
  18. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/_bootstrap_runner.py +0 -0
  19. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/__init__.py +0 -0
  20. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/__init__.py +0 -0
  21. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/apply.py +0 -0
  22. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/check.py +0 -0
  23. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/clone.py +0 -0
  24. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/init.py +0 -0
  25. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/migrate.py +0 -0
  26. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/patch.py +0 -0
  27. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/recover.py +0 -0
  28. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/restore.py +0 -0
  29. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  30. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/rollback.py +0 -0
  31. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  32. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/sync.py +0 -0
  33. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/todo.py +0 -0
  34. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/undo.py +0 -0
  35. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/commands/upgrade.py +0 -0
  36. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli/main.py +0 -0
  37. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/cli_extension.py +0 -0
  38. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/decorators.py +0 -0
  39. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/hgit.py +0 -0
  40. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/migration_manager.py +0 -0
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/modules.py +0 -0
  52. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patch_manager.py +0 -0
  53. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patch_validator.py +0 -0
  54. {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
  55. {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
  56. {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
  57. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/log +0 -0
  58. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  59. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/py.typed +0 -0
  60. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/release_file.py +0 -0
  61. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/scripts/repair-metadata.py +0 -0
  62. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/.gitignore +0 -0
  63. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/MANIFEST.in +0 -0
  64. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/README +0 -0
  65. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/conftest_template +0 -0
  66. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  67. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/pre-push +0 -0
  68. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  69. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
  70. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/init_module_template +0 -0
  71. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_1 +0 -0
  72. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_2 +0 -0
  73. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/module_template_3 +0 -0
  74. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/pyproject.toml +0 -0
  75. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/relation_test +0 -0
  76. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/sql_adapter +0 -0
  77. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/templates/warning +0 -0
  78. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/utils.py +0 -0
  79. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev/venv_setup.py +0 -0
  80. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  81. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/entry_points.txt +0 -0
  82. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/half_orm_dev.egg-info/top_level.txt +0 -0
  83. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/pyproject.toml +0 -0
  84. {half_orm_dev-1.0.0a33 → half_orm_dev-1.0.0a35}/setup.cfg +0 -0
  85. {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.0a33
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.0rc16
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 metadata-X.Y.Z.sql
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 metadata-1.3.5.sql
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
- # to avoid version-specific SET commands
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
- raise FileExecutionError(f"psql execution failed for {file_path.name}: {e}") from e
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 metadata-{version_str}.sql")
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
- - **Development**: Each `patch apply` (allows iteration on bootstrap)
2367
- - **Production**: Initial `clone` only (one-time setup)
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
- For production data changes, use **patches** (not bootstrap).
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
- # Add user schemas from half_orm metadata
2641
- relations = self.model.desc()
2642
- _ = [schemas_to_drop.add(rel[1][1]) for rel in relations]
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(f"Failed to load schema from {schema_path.name}: {e}") from e
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}: {e}"
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(f"Failed to load schema from {schema_path.name}: {e}") from e
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}: {e}"
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(f"Failed to load dump from {dump_path.name}: {e}") from e
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(f"Failed to restore from release schema: {e}") from e
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.0a33
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.0rc16
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
@@ -4,7 +4,7 @@ packaging
4
4
  pydash
5
5
  pytest
6
6
  pytest-asyncio
7
- half_orm<1.1.0,>=1.0.0rc16
7
+ half_orm<1.1.0,>=1.0.0
8
8
  tomli_w>=1.0.0
9
9
 
10
10
  [:python_version < "3.11"]
@@ -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.0rc16'), # 1.0.0+ requires half_orm >= 1.0.0rc16 (with_half_orm_meta support)
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