fix(dn): reconcile staged ingestion indexes by name

This commit is contained in:
DEls 2026-09-18 19:02:47 +02:00
parent ad23b6572e
commit 498b25858a
2 changed files with 56 additions and 10 deletions

View file

@ -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 "

View file

@ -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 };
}