diff --git a/scripts/pb-add-dn-staged-ingestion.py b/scripts/pb-add-dn-staged-ingestion.py index bbf027c..5d815de 100644 --- a/scripts/pb-add-dn-staged-ingestion.py +++ b/scripts/pb-add-dn-staged-ingestion.py @@ -5,6 +5,7 @@ The migration is idempotent and is a dry run unless ``--apply`` is supplied. It reads existing PocketBase fields and indexes before appending anything, so running it against a live instance preserves unrelated schema changes. """ +import re import sys from pb_env import PB, http, superuser_token @@ -51,6 +52,32 @@ def field(name, field_type="text", **options): return {"name": name, "type": field_type, "required": False, **options} +def index_name(statement): + """Extract an SQLite/PocketBase index name from a CREATE INDEX statement.""" + match = re.search(r"\bINDEX\s+`?([^`\s]+)`?", statement, re.IGNORECASE) + if not match: + raise ValueError(f"not a CREATE INDEX statement: {statement!r}") + return match.group(1) + + +def merge_indexes(existing, wanted): + """Union indexes by name, replacing stale same-name definitions in place.""" + merged = list(existing) + positions = {index_name(statement): i for i, statement in enumerate(merged)} + changes = [] + for statement in wanted: + name = index_name(statement) + position = positions.get(name) + if position is None: + positions[name] = len(merged) + merged.append(statement) + changes.append(("add", statement)) + elif merged[position] != statement: + merged[position] = statement + changes.append(("replace", statement)) + return merged, changes + + def add_fields(token, name, wanted): collection = require_collection(token, name) existing = list(collection.get("fields", [])) @@ -72,15 +99,15 @@ def add_fields(token, name, wanted): def add_indexes(token, name, wanted): collection = require_collection(token, name) existing = list(collection.get("indexes", []) or []) - additions = [item for item in wanted if item not in existing] - if not additions: + merged, changes = merge_indexes(existing, wanted) + if not changes: print(f" {name}: indexes already present") return - print(f" {name}: add indexes {additions}") + print(f" {name}: reconcile indexes {[item for _, item in changes]}") if not DRY_RUN: code, response = http( "PATCH", f"/api/collections/{collection['id']}", - {"indexes": existing + additions}, token, + {"indexes": merged}, token, ) if code != 200: sys.exit(f"PATCH {name} indexes failed: {code} {response}") @@ -113,7 +140,26 @@ def system_fields(): ] +def self_test(): + old = [ + "CREATE INDEX `unrelated` ON `x` (`value`)", + "CREATE UNIQUE INDEX `idx_delivery_notes_batch_note` ON `delivery_notes` (`batch_note`)", + ] + replacement = ( + "CREATE UNIQUE INDEX `idx_delivery_notes_batch_note` ON `delivery_notes` (`batch_note`) " + "WHERE `batch_note` IS NOT NULL AND `batch_note` != ''" + ) + merged, changes = merge_indexes(old, [replacement]) + assert merged == [old[0], replacement] + assert changes == [("replace", replacement)] + assert merge_indexes(merged, [replacement])[1] == [] + print("self-test: index replacement and idempotence passed") + return 0 + + def main(): + if "--self-test" in sys.argv: + return self_test() if APPLY and confirmation_host() != PB: sys.exit( "REFUSING --apply: pass --confirm-host with the exact configured " diff --git a/src/lib/pb-types.ts b/src/lib/pb-types.ts index c87fcc6..e2b1055 100644 --- a/src/lib/pb-types.ts +++ b/src/lib/pb-types.ts @@ -509,12 +509,12 @@ export interface SupplierDocumentAliasRecord extends RecordModel { export interface DnReviewEventRecord extends RecordModel { collectionName: "dn_review_events"; batch: string; - note: string; - action: string; - before: unknown; - after: unknown; - reason: string; - actor: string; + note?: string; + action?: string; + before?: unknown; + after?: unknown; + reason?: string; + actor?: string; expand?: { batch?: DnBatchRecord; note?: DnBatchNoteRecord }; }