half-orm-dev 0.17.3a5__py3-none-any.whl → 0.17.3a7__py3-none-any.whl
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/cli/commands/release.py +103 -24
- half_orm_dev/database.py +66 -5
- half_orm_dev/patch_manager.py +1 -1
- half_orm_dev/release_manager.py +350 -55
- half_orm_dev/repo.py +94 -25
- half_orm_dev/scripts/repair-metadata.py +352 -0
- half_orm_dev/templates/README +36 -2
- half_orm_dev/version.txt +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/METADATA +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/RECORD +14 -13
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/WHEEL +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/licenses/AUTHORS +0 -0
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/licenses/LICENSE +0 -0
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/top_level.txt +0 -0
half_orm_dev/repo.py
CHANGED
|
@@ -2218,17 +2218,19 @@ See docs/half_orm_dev.md for complete documentation.
|
|
|
2218
2218
|
|
|
2219
2219
|
def restore_database_from_schema(self) -> None:
|
|
2220
2220
|
"""
|
|
2221
|
-
Restore database from model/schema.sql and
|
|
2221
|
+
Restore database from model/schema.sql, metadata, and data files.
|
|
2222
2222
|
|
|
2223
2223
|
Restores database to clean production state by dropping all user schemas
|
|
2224
|
-
|
|
2224
|
+
and loading schema, metadata, and reference data. Used for from-scratch
|
|
2225
|
+
installations (clone) and patch development (patch apply).
|
|
2225
2226
|
|
|
2226
2227
|
Process:
|
|
2227
2228
|
1. Verify model/schema.sql exists (file or symlink)
|
|
2228
2229
|
2. Drop all user schemas with CASCADE (no superuser privileges needed)
|
|
2229
2230
|
3. Load schema structure from model/schema.sql using psql -f
|
|
2230
2231
|
4. Load half_orm_meta data from model/metadata-X.Y.Z.sql using psql -f (if exists)
|
|
2231
|
-
5.
|
|
2232
|
+
5. Load reference data from model/data-*.sql files up to current version
|
|
2233
|
+
6. Reload halfORM Model metadata cache
|
|
2232
2234
|
|
|
2233
2235
|
The method uses DROP SCHEMA CASCADE instead of dropdb/createdb, allowing
|
|
2234
2236
|
operation without CREATEDB privilege or superuser access. This makes it
|
|
@@ -2238,20 +2240,24 @@ See docs/half_orm_dev.md for complete documentation.
|
|
|
2238
2240
|
- Accepts model/schema.sql as regular file or symlink
|
|
2239
2241
|
- Symlink typically points to versioned schema-X.Y.Z.sql file
|
|
2240
2242
|
- Follows symlink automatically during psql execution
|
|
2241
|
-
- Deduces
|
|
2242
|
-
-
|
|
2243
|
+
- Deduces version from schema.sql symlink target for metadata and data files
|
|
2244
|
+
- Missing metadata/data files are silently skipped (backward compatibility)
|
|
2245
|
+
|
|
2246
|
+
Data Files:
|
|
2247
|
+
- model/data-X.Y.Z.sql contains reference data from @HOP:data patches
|
|
2248
|
+
- All data files up to current version are loaded in version order
|
|
2249
|
+
- Example: for version 1.2.0, loads data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql
|
|
2243
2250
|
|
|
2244
2251
|
Error Handling:
|
|
2245
2252
|
- Raises RepoError if model/schema.sql not found
|
|
2246
2253
|
- Raises RepoError if schema drop fails
|
|
2247
|
-
- Raises RepoError if psql schema load fails
|
|
2248
|
-
- Raises RepoError if psql metadata load fails (when file exists)
|
|
2254
|
+
- Raises RepoError if psql schema/metadata/data load fails
|
|
2249
2255
|
- Database state rolled back on any failure
|
|
2250
2256
|
|
|
2251
2257
|
Usage Context:
|
|
2258
|
+
- Called by clone_repo workflow (from-scratch installation)
|
|
2252
2259
|
- Called by apply-patch workflow (Step 1: Database Restoration)
|
|
2253
|
-
- Ensures clean state before applying
|
|
2254
|
-
- Part of isolated patch testing strategy
|
|
2260
|
+
- Ensures clean state with all reference data before applying patches
|
|
2255
2261
|
|
|
2256
2262
|
Returns:
|
|
2257
2263
|
None
|
|
@@ -2263,31 +2269,23 @@ See docs/half_orm_dev.md for complete documentation.
|
|
|
2263
2269
|
Examples:
|
|
2264
2270
|
# Restore database from model/schema.sql before applying patch
|
|
2265
2271
|
repo.restore_database_from_schema()
|
|
2266
|
-
# Database now contains
|
|
2272
|
+
# Database now contains: schema + metadata + reference data
|
|
2267
2273
|
|
|
2268
2274
|
# Typical apply-patch workflow
|
|
2269
|
-
repo.restore_database_from_schema() # Step 1: Clean state +
|
|
2275
|
+
repo.restore_database_from_schema() # Step 1: Clean state + all data
|
|
2270
2276
|
patch_mgr.apply_patch_files("456-user-auth", repo.model) # Step 2: Apply patch
|
|
2271
2277
|
|
|
2272
|
-
# With versioned
|
|
2278
|
+
# With versioned files
|
|
2273
2279
|
# If schema.sql → schema-1.2.3.sql exists
|
|
2274
|
-
# Then metadata-1.2.3.sql
|
|
2275
|
-
|
|
2276
|
-
# Error handling
|
|
2277
|
-
try:
|
|
2278
|
-
repo.restore_database_from_schema()
|
|
2279
|
-
except RepoError as e:
|
|
2280
|
-
print(f"Database restoration failed: {e}")
|
|
2281
|
-
# Handle error: check schema.sql exists, verify permissions
|
|
2280
|
+
# Then loads: metadata-1.2.3.sql, data-0.1.0.sql, data-1.0.0.sql, data-1.2.3.sql
|
|
2282
2281
|
|
|
2283
2282
|
Notes:
|
|
2284
2283
|
- Uses DROP SCHEMA CASCADE - no superuser or CREATEDB privilege required
|
|
2285
2284
|
- Works on cloud databases (AWS RDS, Azure Database, etc.)
|
|
2286
2285
|
- Uses Model.reconnect(reload=True) to refresh metadata cache
|
|
2287
2286
|
- Supports both schema.sql file and schema.sql -> schema-X.Y.Z.sql symlink
|
|
2288
|
-
- Metadata
|
|
2287
|
+
- Metadata and data files are optional (backward compatibility)
|
|
2289
2288
|
- All PostgreSQL commands use repository connection configuration
|
|
2290
|
-
- Version deduction: schema.sql → schema-1.2.3.sql ⇒ metadata-1.2.3.sql
|
|
2291
2289
|
"""
|
|
2292
2290
|
# 1. Verify model/schema.sql exists
|
|
2293
2291
|
schema_path = Path(self.model_dir) / "schema.sql"
|
|
@@ -2318,15 +2316,16 @@ See docs/half_orm_dev.md for complete documentation.
|
|
|
2318
2316
|
self.database.execute_pg_command(
|
|
2319
2317
|
'psql', '-d', self.name, '-f', str(metadata_path)
|
|
2320
2318
|
)
|
|
2321
|
-
# Optional: Log success (can be removed if too verbose)
|
|
2322
|
-
# print(f"✓ Loaded metadata from {metadata_path.name}")
|
|
2323
2319
|
except Exception as e:
|
|
2324
2320
|
raise RepoError(
|
|
2325
2321
|
f"Failed to load metadata from {metadata_path.name}: {e}"
|
|
2326
2322
|
) from e
|
|
2327
2323
|
# else: metadata file doesn't exist, continue without error (backward compatibility)
|
|
2328
2324
|
|
|
2329
|
-
# 5.
|
|
2325
|
+
# 5. Load data files from model/data-*.sql (all versions up to current)
|
|
2326
|
+
self._load_data_files(schema_path)
|
|
2327
|
+
|
|
2328
|
+
# 6. Reload half_orm metadata cache
|
|
2330
2329
|
self.model.reconnect(reload=True)
|
|
2331
2330
|
|
|
2332
2331
|
except RepoError:
|
|
@@ -2380,6 +2379,76 @@ See docs/half_orm_dev.md for complete documentation.
|
|
|
2380
2379
|
|
|
2381
2380
|
return metadata_path
|
|
2382
2381
|
|
|
2382
|
+
def _load_data_files(self, schema_path: Path) -> None:
|
|
2383
|
+
"""
|
|
2384
|
+
Load all data files from model/data-*.sql up to current version.
|
|
2385
|
+
|
|
2386
|
+
Data files contain reference data (DML) from patches with @HOP:data annotation.
|
|
2387
|
+
They are loaded in version order for from-scratch installations.
|
|
2388
|
+
|
|
2389
|
+
Args:
|
|
2390
|
+
schema_path: Path to model/schema.sql (used to deduce current version)
|
|
2391
|
+
|
|
2392
|
+
Process:
|
|
2393
|
+
1. Deduce current version from schema.sql symlink
|
|
2394
|
+
2. Find all data-*.sql files in model/
|
|
2395
|
+
3. Sort by version (semantic versioning)
|
|
2396
|
+
4. Load each file up to current version using psql -f
|
|
2397
|
+
|
|
2398
|
+
Examples:
|
|
2399
|
+
# schema.sql → schema-1.2.0.sql
|
|
2400
|
+
# model/ contains: data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql, data-2.0.0.sql
|
|
2401
|
+
# Loads: data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql (skips 2.0.0)
|
|
2402
|
+
"""
|
|
2403
|
+
# Deduce current version from schema.sql symlink
|
|
2404
|
+
if not schema_path.is_symlink():
|
|
2405
|
+
return # No version info, skip data loading
|
|
2406
|
+
|
|
2407
|
+
try:
|
|
2408
|
+
target = Path(os.readlink(schema_path))
|
|
2409
|
+
except OSError:
|
|
2410
|
+
return
|
|
2411
|
+
|
|
2412
|
+
match = re.match(r'schema-(\d+\.\d+\.\d+)\.sql$', target.name)
|
|
2413
|
+
if not match:
|
|
2414
|
+
return
|
|
2415
|
+
|
|
2416
|
+
current_version = match.group(1)
|
|
2417
|
+
current_tuple = tuple(map(int, current_version.split('.')))
|
|
2418
|
+
|
|
2419
|
+
# Find all data files
|
|
2420
|
+
model_dir = schema_path.parent
|
|
2421
|
+
data_files = list(model_dir.glob("data-*.sql"))
|
|
2422
|
+
|
|
2423
|
+
if not data_files:
|
|
2424
|
+
return # No data files to load
|
|
2425
|
+
|
|
2426
|
+
# Parse and sort by version
|
|
2427
|
+
versioned_files = []
|
|
2428
|
+
for data_file in data_files:
|
|
2429
|
+
match = re.match(r'data-(\d+\.\d+\.\d+)\.sql$', data_file.name)
|
|
2430
|
+
if match:
|
|
2431
|
+
version = match.group(1)
|
|
2432
|
+
version_tuple = tuple(map(int, version.split('.')))
|
|
2433
|
+
versioned_files.append((version_tuple, data_file))
|
|
2434
|
+
|
|
2435
|
+
# Sort by version tuple
|
|
2436
|
+
versioned_files.sort(key=lambda x: x[0])
|
|
2437
|
+
|
|
2438
|
+
# Load each file up to current version
|
|
2439
|
+
for version_tuple, data_file in versioned_files:
|
|
2440
|
+
if version_tuple > current_tuple:
|
|
2441
|
+
break # Stop at versions beyond current
|
|
2442
|
+
|
|
2443
|
+
try:
|
|
2444
|
+
self.database.execute_pg_command(
|
|
2445
|
+
'psql', '-d', self.name, '-f', str(data_file)
|
|
2446
|
+
)
|
|
2447
|
+
except Exception as e:
|
|
2448
|
+
raise RepoError(
|
|
2449
|
+
f"Failed to load data from {data_file.name}: {e}"
|
|
2450
|
+
) from e
|
|
2451
|
+
|
|
2383
2452
|
@classmethod
|
|
2384
2453
|
def clone_repo(cls,
|
|
2385
2454
|
git_origin: str,
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Repair script for half-orm-dev metadata files.
|
|
4
|
+
|
|
5
|
+
This script regenerates all metadata-X.Y.Z.sql files with correct hop_release
|
|
6
|
+
entries by:
|
|
7
|
+
1. Clearing half_orm_meta.hop_release (keeping only 0.0.0)
|
|
8
|
+
2. For each version tag (vX.Y.Z or vX.Y.Z-rcN):
|
|
9
|
+
- Insert the version with the tag's date
|
|
10
|
+
- Generate metadata-X.Y.Z.sql (or metadata-X.Y.Z-rcN.sql)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
cd /path/to/your/project
|
|
14
|
+
python /path/to/half-orm-dev/scripts/repair-metadata.py [--dry-run]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--dry-run Show what would be done without modifying anything
|
|
18
|
+
--verbose Show detailed information
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import List, Tuple, Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_version_tags() -> List[Tuple[str, str, datetime]]:
|
|
31
|
+
"""
|
|
32
|
+
Get all version tags with their dates from git.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of (tag_name, version_string, date) tuples sorted by version.
|
|
36
|
+
tag_name: e.g., "v0.1.0", "v0.1.0-rc1"
|
|
37
|
+
version_string: e.g., "0.1.0", "0.1.0-rc1"
|
|
38
|
+
date: datetime object
|
|
39
|
+
"""
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
['git', 'tag', '--format=%(refname:short) %(creatordate:iso)'],
|
|
42
|
+
capture_output=True, text=True, check=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
tags = []
|
|
46
|
+
for line in result.stdout.strip().split('\n'):
|
|
47
|
+
if not line.strip():
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
parts = line.split(' ', 1)
|
|
51
|
+
if len(parts) != 2:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
tag_name, date_str = parts
|
|
55
|
+
|
|
56
|
+
# Match vX.Y.Z or vX.Y.Z-rcN
|
|
57
|
+
match = re.match(r'^v(\d+\.\d+\.\d+(?:-rc\d+)?)$', tag_name)
|
|
58
|
+
if not match:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
version_str = match.group(1)
|
|
62
|
+
|
|
63
|
+
# Parse date (format: "2025-11-21 15:09:14 +0100")
|
|
64
|
+
try:
|
|
65
|
+
# Remove timezone for simpler parsing
|
|
66
|
+
date_part = ' '.join(date_str.split()[:2])
|
|
67
|
+
date = datetime.strptime(date_part, '%Y-%m-%d %H:%M:%S')
|
|
68
|
+
except ValueError:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
tags.append((tag_name, version_str, date))
|
|
72
|
+
|
|
73
|
+
# Sort by version (handle X.Y.Z and X.Y.Z-rcN)
|
|
74
|
+
def version_key(item):
|
|
75
|
+
version_str = item[1]
|
|
76
|
+
# Split base version and rc part
|
|
77
|
+
if '-rc' in version_str:
|
|
78
|
+
base, rc = version_str.split('-rc')
|
|
79
|
+
rc_num = int(rc)
|
|
80
|
+
else:
|
|
81
|
+
base = version_str
|
|
82
|
+
rc_num = 9999 # Production comes after all RCs
|
|
83
|
+
|
|
84
|
+
parts = [int(p) for p in base.split('.')]
|
|
85
|
+
return (*parts, rc_num)
|
|
86
|
+
|
|
87
|
+
return sorted(tags, key=version_key)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_version_string(version_str: str) -> Tuple[int, int, int, str, str]:
|
|
91
|
+
"""
|
|
92
|
+
Parse version string into components.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
version_str: e.g., "0.1.0" or "0.1.0-rc1"
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple of (major, minor, patch, pre_release, pre_release_num)
|
|
99
|
+
"""
|
|
100
|
+
if '-rc' in version_str:
|
|
101
|
+
base, rc = version_str.split('-rc')
|
|
102
|
+
pre_release = 'rc'
|
|
103
|
+
pre_release_num = rc
|
|
104
|
+
else:
|
|
105
|
+
base = version_str
|
|
106
|
+
pre_release = ''
|
|
107
|
+
pre_release_num = ''
|
|
108
|
+
|
|
109
|
+
parts = base.split('.')
|
|
110
|
+
return (int(parts[0]), int(parts[1]), int(parts[2]), pre_release, pre_release_num)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_model_dir() -> Optional[Path]:
|
|
114
|
+
"""Find the .hop/model directory in current working directory."""
|
|
115
|
+
cwd = Path.cwd()
|
|
116
|
+
model_dir = cwd / ".hop" / "model"
|
|
117
|
+
|
|
118
|
+
if model_dir.is_dir():
|
|
119
|
+
return model_dir
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_database_name() -> Optional[str]:
|
|
124
|
+
"""Get database name from half_orm config."""
|
|
125
|
+
try:
|
|
126
|
+
from half_orm.model import Model
|
|
127
|
+
model = Model._model
|
|
128
|
+
if model:
|
|
129
|
+
return model._dbname
|
|
130
|
+
except:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# Try reading from .hop/config
|
|
134
|
+
config_path = Path.cwd() / ".hop" / "config"
|
|
135
|
+
if config_path.exists():
|
|
136
|
+
import configparser
|
|
137
|
+
config = configparser.ConfigParser()
|
|
138
|
+
config.read(config_path)
|
|
139
|
+
if 'database' in config and 'name' in config['database']:
|
|
140
|
+
return config['database']['name']
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def clear_hop_release_table(db_name: str, dry_run: bool = False) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Clear hop_release table keeping only 0.0.0.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
db_name: Database name
|
|
151
|
+
dry_run: If True, only show what would be done
|
|
152
|
+
"""
|
|
153
|
+
sql = "DELETE FROM half_orm_meta.hop_release WHERE NOT (major = 0 AND minor = 0 AND patch = 0);"
|
|
154
|
+
|
|
155
|
+
if dry_run:
|
|
156
|
+
print(f"Would execute: {sql}")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
subprocess.run(
|
|
160
|
+
['psql', '-d', db_name, '-c', sql],
|
|
161
|
+
check=True, capture_output=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def insert_version(db_name: str, major: int, minor: int, patch: int,
|
|
166
|
+
pre_release: str, pre_release_num: str,
|
|
167
|
+
date: datetime, dry_run: bool = False) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Insert a version into hop_release table.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
db_name: Database name
|
|
173
|
+
major, minor, patch: Version components
|
|
174
|
+
pre_release: 'rc' or ''
|
|
175
|
+
pre_release_num: RC number or ''
|
|
176
|
+
date: Release date
|
|
177
|
+
dry_run: If True, only show what would be done
|
|
178
|
+
"""
|
|
179
|
+
date_str = date.strftime('%Y-%m-%d')
|
|
180
|
+
time_str = date.strftime('%H:%M:%S')
|
|
181
|
+
|
|
182
|
+
sql = f"""INSERT INTO half_orm_meta.hop_release
|
|
183
|
+
(major, minor, patch, pre_release, pre_release_num, date, time)
|
|
184
|
+
VALUES ({major}, {minor}, {patch}, '{pre_release}', '{pre_release_num}', '{date_str}', '{time_str}');"""
|
|
185
|
+
|
|
186
|
+
if dry_run:
|
|
187
|
+
print(f"Would execute: {sql}")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
subprocess.run(
|
|
191
|
+
['psql', '-d', db_name, '-c', sql],
|
|
192
|
+
check=True, capture_output=True
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def generate_metadata_file(db_name: str, version_str: str, model_dir: Path,
|
|
197
|
+
dry_run: bool = False) -> Path:
|
|
198
|
+
"""
|
|
199
|
+
Generate metadata-X.Y.Z.sql file using pg_dump.
|
|
200
|
+
|
|
201
|
+
Only keeps COPY blocks to avoid version-specific SET commands
|
|
202
|
+
and ensure compatibility across PostgreSQL versions.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
db_name: Database name
|
|
206
|
+
version_str: Version string (e.g., "0.1.0" or "0.1.0-rc1")
|
|
207
|
+
model_dir: Path to model directory
|
|
208
|
+
dry_run: If True, only show what would be done
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Path to generated file
|
|
212
|
+
"""
|
|
213
|
+
metadata_file = model_dir / f"metadata-{version_str}.sql"
|
|
214
|
+
|
|
215
|
+
if dry_run:
|
|
216
|
+
print(f"Would generate: {metadata_file}")
|
|
217
|
+
return metadata_file
|
|
218
|
+
|
|
219
|
+
# Dump to stdout
|
|
220
|
+
result = subprocess.run([
|
|
221
|
+
'pg_dump', db_name,
|
|
222
|
+
'--data-only',
|
|
223
|
+
'--table=half_orm_meta.database',
|
|
224
|
+
'--table=half_orm_meta.hop_release',
|
|
225
|
+
'--table=half_orm_meta.hop_release_issue',
|
|
226
|
+
], check=True, capture_output=True, text=True)
|
|
227
|
+
|
|
228
|
+
# Filter to keep only COPY blocks (COPY ... FROM stdin; ... \.)
|
|
229
|
+
filtered_lines = []
|
|
230
|
+
in_copy_block = False
|
|
231
|
+
for line in result.stdout.split('\n'):
|
|
232
|
+
if line.startswith('COPY '):
|
|
233
|
+
in_copy_block = True
|
|
234
|
+
if in_copy_block:
|
|
235
|
+
filtered_lines.append(line)
|
|
236
|
+
if line == '\\.':
|
|
237
|
+
in_copy_block = False
|
|
238
|
+
filtered_lines.append('') # Empty line between blocks
|
|
239
|
+
|
|
240
|
+
metadata_file.write_text('\n'.join(filtered_lines))
|
|
241
|
+
|
|
242
|
+
return metadata_file
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main():
|
|
246
|
+
parser = argparse.ArgumentParser(
|
|
247
|
+
description="Regenerate half-orm-dev metadata files from git tags.",
|
|
248
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
249
|
+
epilog=__doc__
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
'--dry-run',
|
|
253
|
+
action='store_true',
|
|
254
|
+
help='Show what would be done without modifying anything'
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
'--verbose',
|
|
258
|
+
action='store_true',
|
|
259
|
+
help='Show detailed information'
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
'--database',
|
|
263
|
+
type=str,
|
|
264
|
+
help='Database name (auto-detected if not specified)'
|
|
265
|
+
)
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
'--model-dir',
|
|
268
|
+
type=Path,
|
|
269
|
+
help='Path to model directory (default: .hop/model)'
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
args = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
# Find model directory
|
|
275
|
+
if args.model_dir:
|
|
276
|
+
model_dir = args.model_dir
|
|
277
|
+
else:
|
|
278
|
+
model_dir = find_model_dir()
|
|
279
|
+
|
|
280
|
+
if model_dir is None or not model_dir.is_dir():
|
|
281
|
+
print("Error: Could not find .hop/model directory", file=sys.stderr)
|
|
282
|
+
print("Make sure you're in a half-orm-dev managed project directory", file=sys.stderr)
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
|
|
285
|
+
# Get database name
|
|
286
|
+
db_name = args.database or get_database_name()
|
|
287
|
+
if not db_name:
|
|
288
|
+
print("Error: Could not determine database name", file=sys.stderr)
|
|
289
|
+
print("Use --database option to specify it", file=sys.stderr)
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
|
|
292
|
+
print(f"Model directory: {model_dir}")
|
|
293
|
+
print(f"Database: {db_name}")
|
|
294
|
+
print()
|
|
295
|
+
|
|
296
|
+
if args.dry_run:
|
|
297
|
+
print("=== DRY RUN MODE - No changes will be made ===")
|
|
298
|
+
print()
|
|
299
|
+
|
|
300
|
+
# Get version tags
|
|
301
|
+
tags = get_version_tags()
|
|
302
|
+
|
|
303
|
+
if not tags:
|
|
304
|
+
print("No version tags found (expected format: vX.Y.Z or vX.Y.Z-rcN)")
|
|
305
|
+
sys.exit(0)
|
|
306
|
+
|
|
307
|
+
print(f"Found {len(tags)} version tags")
|
|
308
|
+
print()
|
|
309
|
+
|
|
310
|
+
# Step 1: Clear hop_release table (keep 0.0.0)
|
|
311
|
+
print("Step 1: Clearing hop_release table (keeping 0.0.0)...")
|
|
312
|
+
clear_hop_release_table(db_name, dry_run=args.dry_run)
|
|
313
|
+
print(" Done")
|
|
314
|
+
print()
|
|
315
|
+
|
|
316
|
+
# Step 2: Process each version
|
|
317
|
+
print("Step 2: Processing versions...")
|
|
318
|
+
for tag_name, version_str, date in tags:
|
|
319
|
+
major, minor, patch, pre_release, pre_release_num = parse_version_string(version_str)
|
|
320
|
+
|
|
321
|
+
if args.verbose:
|
|
322
|
+
print(f" {tag_name} -> {version_str} ({date})")
|
|
323
|
+
|
|
324
|
+
# Insert version with correct date
|
|
325
|
+
insert_version(
|
|
326
|
+
db_name, major, minor, patch,
|
|
327
|
+
pre_release, pre_release_num, date,
|
|
328
|
+
dry_run=args.dry_run
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Generate metadata file
|
|
332
|
+
metadata_file = generate_metadata_file(
|
|
333
|
+
db_name, version_str, model_dir,
|
|
334
|
+
dry_run=args.dry_run
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
print(f" ✓ {version_str} ({date.strftime('%Y-%m-%d')})")
|
|
338
|
+
|
|
339
|
+
print()
|
|
340
|
+
print("=" * 50)
|
|
341
|
+
print(f"Summary:")
|
|
342
|
+
print(f" Versions processed: {len(tags)}")
|
|
343
|
+
|
|
344
|
+
if not args.dry_run:
|
|
345
|
+
print()
|
|
346
|
+
print("Next steps:")
|
|
347
|
+
print(" 1. Review the changes: git status")
|
|
348
|
+
print(" 2. Commit: git add .hop/model/metadata-*.sql && git commit -m 'fix: regenerate metadata files'")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
main()
|
half_orm_dev/templates/README
CHANGED
|
@@ -170,10 +170,11 @@ half_orm dev upgrade <version> # Deploy specific version
|
|
|
170
170
|
{package_name}/
|
|
171
171
|
├── .hop/ # half-orm-dev metadata
|
|
172
172
|
│ ├── config # Repository configuration
|
|
173
|
-
│ ├── model/ # Database schemas
|
|
173
|
+
│ ├── model/ # Database schemas and data
|
|
174
174
|
│ │ ├── schema.sql # Current production schema (symlink)
|
|
175
175
|
│ │ ├── schema-X.Y.Z.sql # Versioned schemas
|
|
176
|
-
│ │
|
|
176
|
+
│ │ ├── metadata-X.Y.Z.sql # half_orm_meta data dumps
|
|
177
|
+
│ │ └── data-X.Y.Z.sql # Reference data from @HOP:data patches
|
|
177
178
|
│ └── releases/ # Release tracking files
|
|
178
179
|
│ ├── X.Y.Z-patches.toml # Development releases (mutable)
|
|
179
180
|
│ ├── X.Y.Z-rcN.txt # Release candidates (immutable)
|
|
@@ -199,6 +200,39 @@ All development happens on patch branches, merged into release branches, then pr
|
|
|
199
200
|
|
|
200
201
|
---
|
|
201
202
|
|
|
203
|
+
## 💾 Data Persistence (@HOP:data)
|
|
204
|
+
|
|
205
|
+
For reference data that must be loaded with every database installation (lookup tables, default roles, etc.), use the `@HOP:data` annotation:
|
|
206
|
+
|
|
207
|
+
```sql
|
|
208
|
+
-- @HOP:data
|
|
209
|
+
-- This file will be included in model/data-X.Y.Z.sql
|
|
210
|
+
|
|
211
|
+
INSERT INTO roles (name, description)
|
|
212
|
+
VALUES ('admin', 'Administrator')
|
|
213
|
+
ON CONFLICT (name) DO NOTHING;
|
|
214
|
+
|
|
215
|
+
INSERT INTO permissions (name)
|
|
216
|
+
VALUES ('read'), ('write'), ('delete')
|
|
217
|
+
ON CONFLICT DO NOTHING;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### How it works
|
|
221
|
+
|
|
222
|
+
1. **In patches**: SQL files starting with `-- @HOP:data` contain reference data
|
|
223
|
+
2. **Production promote**: All `@HOP:data` files are consolidated into `model/data-X.Y.Z.sql`
|
|
224
|
+
3. **Clone/Restore**: Data files are loaded automatically after schema restoration
|
|
225
|
+
4. **Production upgrade**: Data is inserted via normal patch application (no special handling)
|
|
226
|
+
|
|
227
|
+
### Best practices
|
|
228
|
+
|
|
229
|
+
- Use `ON CONFLICT DO NOTHING` or `ON CONFLICT DO UPDATE` for idempotency
|
|
230
|
+
- Keep data files small and focused (one concern per file)
|
|
231
|
+
- Number your SQL files to control execution order: `01_roles.sql`, `02_permissions.sql`
|
|
232
|
+
- Only use for **reference data**, not user-generated data
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
202
236
|
## 📚 Documentation
|
|
203
237
|
|
|
204
238
|
- **half-orm-dev**: https://github.com/half-orm/half-orm-dev
|
half_orm_dev/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.17.3-
|
|
1
|
+
0.17.3-a7
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
half_orm_dev/__init__.py,sha256=0JpUPey1gacxXuIFGcpD2nTGso73fkak72qzTHttAJk,18
|
|
2
2
|
half_orm_dev/cli_extension.py,sha256=kwX3M11_rwr0pFcqHK_bpI3Pp4ztfTCVz2gLfTmzfeA,1066
|
|
3
|
-
half_orm_dev/database.py,sha256=
|
|
3
|
+
half_orm_dev/database.py,sha256=0sq96tseMUWAluoxoxwbTIJrzvBYMzW163D8Ssdj73g,62793
|
|
4
4
|
half_orm_dev/decorators.py,sha256=JKv_Z_JZUr-s-Vz551temHZhhecPfbvyhTbByRDjVAQ,4901
|
|
5
5
|
half_orm_dev/hgit.py,sha256=VdzCCQ__xG1IGJaGq4-rrhbA1bNkDw_dBqkUNIeTONg,58045
|
|
6
6
|
half_orm_dev/migration_manager.py,sha256=9RpciH8nyQrF0xV31kAeaYKkQl24Di1VHt-mAjjHhzM,14854
|
|
7
7
|
half_orm_dev/modules.py,sha256=4jfVb2yboRgb9mcO0sMF-iLigcZFTHEm4VRLN6GQXM4,16796
|
|
8
|
-
half_orm_dev/patch_manager.py,sha256=
|
|
8
|
+
half_orm_dev/patch_manager.py,sha256=HAnQR4m8E0hyKCpPme_XiHI4K7qFkCT2DvSgCaa524s,100993
|
|
9
9
|
half_orm_dev/patch_validator.py,sha256=QNe1L6k_xwsnrOTcb3vkW2D0LbqrCRcZOGPnVyspVRk,10871
|
|
10
10
|
half_orm_dev/release_file.py,sha256=0c9NBhAQ6YpiC3HWj8VtZcfvvZxW2ITk1NEQ60AO0sI,9880
|
|
11
|
-
half_orm_dev/release_manager.py,sha256=
|
|
12
|
-
half_orm_dev/repo.py,sha256=
|
|
11
|
+
half_orm_dev/release_manager.py,sha256=AXjjtIWAdv0yy6DkP_eGYKqDDvejmbxnz_QiMRLvwws,125510
|
|
12
|
+
half_orm_dev/repo.py,sha256=h-nsB6z2xph9ortO02g9DcPXeef9Rk2D5WI3Yc3h1M4,98253
|
|
13
13
|
half_orm_dev/utils.py,sha256=M3yViUFfsO7Cp9MYSoUSkCZ6R9w_4jW45UDZUOT8FhI,1493
|
|
14
|
-
half_orm_dev/version.txt,sha256=
|
|
14
|
+
half_orm_dev/version.txt,sha256=3Ff9bgyfOMEi-lwqWRSvH9yY70F6xBUQBQXzA7QjBbI,10
|
|
15
15
|
half_orm_dev/cli/__init__.py,sha256=0CbMj8OIhZmglWakK7NhYPn302erUTEg2VHOdm1hRTQ,163
|
|
16
16
|
half_orm_dev/cli/main.py,sha256=3SVTl5WraNTSY6o7LfvE1dUHKg_RcuVaHHDIn_oINv4,11701
|
|
17
17
|
half_orm_dev/cli/commands/__init__.py,sha256=UhWf0AnWqy4gyFo2SJQv8pL_YJ43pE_c9TgopcjzKDg,1490
|
|
@@ -21,7 +21,7 @@ half_orm_dev/cli/commands/clone.py,sha256=JUDDt-vz_WvGkm5HDFuZ3KZbclLyPaE4h665n8
|
|
|
21
21
|
half_orm_dev/cli/commands/init.py,sha256=N0TXUL1ExW-DdpNrs4xiXymtSHHLh5fCbPMTBjw2Iwg,12548
|
|
22
22
|
half_orm_dev/cli/commands/migrate.py,sha256=iEz3DoFX22WwaYDo_WUKaF-pFohaLWoUrDmdLCin2wc,4047
|
|
23
23
|
half_orm_dev/cli/commands/patch.py,sha256=sl8mv1mlq-KnKItFV_HBfZ93G5CbdviQRIAYocuMDDo,12810
|
|
24
|
-
half_orm_dev/cli/commands/release.py,sha256=
|
|
24
|
+
half_orm_dev/cli/commands/release.py,sha256=yPp_3NaukhZF3smo6zNsFKtxHwB2RgZnUlomdlM6Sno,17574
|
|
25
25
|
half_orm_dev/cli/commands/restore.py,sha256=n9SP8n1EQUduvDoA0qxpSUQpphc48X-NovnocyGl98I,236
|
|
26
26
|
half_orm_dev/cli/commands/sync.py,sha256=D0Prr8W1ySYjP3D8H4MB05KHccFbhB8z2qB3Bs00swA,274
|
|
27
27
|
half_orm_dev/cli/commands/todo.py,sha256=kL5QU-IjPWmnrKG8L4qk1vb5PDZfY88EFExICiNeLhA,2981
|
|
@@ -35,10 +35,11 @@ half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql,sha256=gMZ94YlyrftxcqDn
|
|
|
35
35
|
half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql,sha256=nhRbDi6sUenvVfOnoRuWSbLEC1cEfzrXbxDof2weq04,183
|
|
36
36
|
half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql,sha256=Bd0lXJ6vC0JNe06yqTWYVSrwVDElCI3McSS5pm-Ijlo,306
|
|
37
37
|
half_orm_dev/patches/sql/half_orm_meta.sql,sha256=Vl2YzEWpWdam-tC0ZE8iNMeTRzEHpxtNhdBThbHb2u4,5864
|
|
38
|
+
half_orm_dev/scripts/repair-metadata.py,sha256=qUP4v5I16ONBfI18_mqyrDIOlpTPoa4MozQSy4-N-lg,10248
|
|
38
39
|
half_orm_dev/templates/.gitignore,sha256=RmvQ9D46T9vpRxhYjjY5WUjGVbuyFUMsH059wC7sPBM,140
|
|
39
40
|
half_orm_dev/templates/MANIFEST.in,sha256=53BeBuKi8UtBWB6IG3VQZk9Ow8Iye6Zs14sP-gVyVDA,25
|
|
40
41
|
half_orm_dev/templates/Pipfile,sha256=u3lGJSk5HZwz-EOTrOdBYrkhGV6zgVtrrRPivrO5rmA,182
|
|
41
|
-
half_orm_dev/templates/README,sha256=
|
|
42
|
+
half_orm_dev/templates/README,sha256=YgNl52Jk7jvwTaB7fLgrXHxOtCECQEl8AqgvImrzaWA,7522
|
|
42
43
|
half_orm_dev/templates/conftest_template,sha256=DopLw67b5cptCYUtmAcQzr5Gz_kzNwpMO6r3goihiks,1206
|
|
43
44
|
half_orm_dev/templates/init_module_template,sha256=o3RAnhGayYUF7NEyI8bcI6JHmAZb2wPVNF-FdrjOnQU,345
|
|
44
45
|
half_orm_dev/templates/module_template_1,sha256=hRa0PiI6-dpBKNXJ9PuDuGocdrq712ujlSJGfJcXOh8,271
|
|
@@ -50,9 +51,9 @@ half_orm_dev/templates/sql_adapter,sha256=kAP5y7Qml3DKsbZLUeoVpeXjbQcWltHjkDznED
|
|
|
50
51
|
half_orm_dev/templates/warning,sha256=4hlZ_rRdpmkXxOeRoVd9xnXBARYXn95e-iXrD1f2u7k,490
|
|
51
52
|
half_orm_dev/templates/git-hooks/pre-commit,sha256=Hf084pqeiOebrv4xzA0aiaHbIXswmmNO-dSIXUfzMK0,4707
|
|
52
53
|
half_orm_dev/templates/git-hooks/prepare-commit-msg,sha256=zknOGGoaWKC97zfga2Xl2i_psnNo9MJbrEBuN91eHNw,1070
|
|
53
|
-
half_orm_dev-0.17.
|
|
54
|
-
half_orm_dev-0.17.
|
|
55
|
-
half_orm_dev-0.17.
|
|
56
|
-
half_orm_dev-0.17.
|
|
57
|
-
half_orm_dev-0.17.
|
|
58
|
-
half_orm_dev-0.17.
|
|
54
|
+
half_orm_dev-0.17.3a7.dist-info/licenses/AUTHORS,sha256=eWxqzRdLOt2gX0FMQj_wui03Od3jdlwa8xNe9tl84g0,113
|
|
55
|
+
half_orm_dev-0.17.3a7.dist-info/licenses/LICENSE,sha256=ufhxlSi6mttkGQTsGWrEoB3WA_fCPJ6-k07GSVBgyPw,644
|
|
56
|
+
half_orm_dev-0.17.3a7.dist-info/METADATA,sha256=w85PxMvXeM6dp9I7qkdppzYR_8bVd3O-6hF3dCfTnG8,16149
|
|
57
|
+
half_orm_dev-0.17.3a7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
58
|
+
half_orm_dev-0.17.3a7.dist-info/top_level.txt,sha256=M5hEsWfn5Kw0HL-VnNmS6Jw-3cwRyjims5a8cr18eTM,13
|
|
59
|
+
half_orm_dev-0.17.3a7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|