WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
"""Insert-only operational history import for the pinned acceptance tenant.
|
||||
|
||||
Authoritative balances and actual transactions are retained independently.
|
||||
Discrepancies remain explicit; no invented credit or shared-user broadcasts.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import uuid
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
from weknora_asset_etl import LEDGER as ASSET_LEDGER, _mapping
|
||||
from weknora_business_etl import archive_text, _canonical
|
||||
from weknora_identity_etl import ADMIN_TARGET_ID, IDENTITY_LEDGER, TARGET_DATABASE, TARGET_TENANT
|
||||
|
||||
SOURCE = "yawan-workbench"
|
||||
LEDGER = "yawan_legacy_operations_mappings"
|
||||
NAMESPACE = uuid.UUID("3fce9870-2fb6-51c6-b727-37ab10674612")
|
||||
KINDS = ("pointaccount", "pointtransaction", "usageevent", "knowledgeusage", "notification",
|
||||
"pendingmemory", "auditlog", "systemlog")
|
||||
TABLES = ("workbench_balances", "workbench_transactions", "workbench_usage", "workbench_consumption",
|
||||
"workbench_events", "workbench_pending_memories", "workbench_operation_logs")
|
||||
NATIVE_TABLE = dict(zip(KINDS, (*TABLES[:6], TABLES[-1], TABLES[-1])))
|
||||
POLICY = "insert_only_authoritative_balances_owned_history_no_inferred_credit"
|
||||
SENSITIVE_KEY = re.compile(r"(?i)(api.?key|authorization|password|passwd|secret|access.?token|refresh.?token|cookie)")
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(r"(?i)(bearer\s+)[A-Za-z0-9._~+/-]{8,}"),
|
||||
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}"),
|
||||
re.compile(r'''(?i)((?:api[_-]?key|authorization|password|passwd|secret|access[_-]?token|refresh[_-]?token)\s*[=:]\s*["']?)[^\s,;"'&}\]]+'''),
|
||||
re.compile(r"(?i)(https?://)[^/\s:@]+:[^/\s@]+@"),
|
||||
)
|
||||
|
||||
|
||||
def operation_id(kind, source_id):
|
||||
if kind not in (*KINDS, "history") or type(source_id) is not int or source_id <= 0:
|
||||
raise ValueError("Invalid operational source identity")
|
||||
return str(uuid.uuid5(NAMESPACE, f"{SOURCE}:tenant:{TARGET_TENANT}:organization:1:{kind}:{source_id}"))
|
||||
|
||||
|
||||
def history_id(kind, source_id):
|
||||
return str(uuid.uuid5(NAMESPACE, f"history:{kind}:{source_id}"))
|
||||
|
||||
|
||||
def redact(value):
|
||||
if isinstance(value, dict):
|
||||
return {k: "[REDACTED]" if SENSITIVE_KEY.search(k) else redact(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [redact(v) for v in value]
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
value = value.replace("\x00", "[NUL]")
|
||||
value = "".join(c if not 0xd800 <= ord(c) <= 0xdfff else "[INVALID-UNICODE]" for c in value)
|
||||
for pattern in SECRET_PATTERNS:
|
||||
value = pattern.sub(lambda m: (m.group(1) if m.lastindex else "") + "[REDACTED]", value)
|
||||
return value
|
||||
|
||||
|
||||
def _read(db, table):
|
||||
if table not in (*KINDS, "campaign", "segment"):
|
||||
raise ValueError("Unreviewed operations source table")
|
||||
result = db.execute(f'SELECT * FROM "{table}" ORDER BY id')
|
||||
fields = [c[0] for c in result.description]
|
||||
rows = [dict(zip(fields, row)) for row in result]
|
||||
for row in rows:
|
||||
if type(row["id"]) is not int or row["id"] <= 0:
|
||||
raise ValueError("Invalid operations source ID")
|
||||
if row.get("organization_id", 1) != 1:
|
||||
raise ValueError("Operations source organization differs")
|
||||
if table in ("pointtransaction", "usageevent"):
|
||||
row["metadata"] = json.loads(row["metadata"] or "{}")
|
||||
return rows
|
||||
|
||||
|
||||
def _time(value):
|
||||
if not value:
|
||||
return "1970-01-01T00:00:00+00:00"
|
||||
date = datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=datetime.timezone.utc)
|
||||
return date.isoformat()
|
||||
|
||||
|
||||
def reconcile(accounts, transactions):
|
||||
totals = defaultdict(int)
|
||||
for tx in transactions:
|
||||
totals[tx["user_id"]] += tx["delta_points"]
|
||||
by_user = {}
|
||||
for account in accounts:
|
||||
if account["user_id"] in by_user:
|
||||
raise ValueError("Duplicate legacy point account")
|
||||
by_user[account["user_id"]] = account
|
||||
result = []
|
||||
for user in sorted(set(totals) | set(by_user)):
|
||||
account = by_user.get(user)
|
||||
balance = account["balance_points"] if account else None
|
||||
matched = account is not None and balance == totals[user]
|
||||
result.append({"source_user_id": user, "source_balance": balance, "transaction_sum": totals[user],
|
||||
"difference": balance - totals[user] if account else None,
|
||||
"reconciled": matched, "native_import": account is not None,
|
||||
"reason": "" if matched else "balance_mismatch" if account else "missing_source_account"})
|
||||
return result
|
||||
|
||||
|
||||
def _relation_owner(row, campaigns, segments, identities):
|
||||
campaign = row.get("campaign_id")
|
||||
segment = row.get("segment_id")
|
||||
if segment:
|
||||
sc = segments.get(segment)
|
||||
if not sc or (campaign and sc != campaign):
|
||||
return "", "segment_campaign_missing_or_mismatch"
|
||||
campaign = sc
|
||||
owner = campaigns.get(campaign)
|
||||
if not owner or str(owner) not in identities:
|
||||
return "", "unresolved_campaign_owner"
|
||||
return identities[str(owner)], ""
|
||||
|
||||
|
||||
def notification_owner(row, campaigns, segments, identities):
|
||||
ref, kind = row["ref_id"], row["type"]
|
||||
if kind == "pending_review":
|
||||
return _relation_owner({"segment_id": ref}, campaigns, segments, identities)
|
||||
if kind == "info":
|
||||
return _relation_owner({"campaign_id": ref}, campaigns, segments, identities)
|
||||
if kind == "failure":
|
||||
candidates = {campaigns[c] for c in (ref, segments.get(ref)) if c in campaigns}
|
||||
if len(candidates) == 1 and str(next(iter(candidates))) in identities:
|
||||
return identities[str(next(iter(candidates)))], ""
|
||||
return "", "ambiguous_or_missing_notification_reference"
|
||||
return "", "notification_has_no_reliable_recipient"
|
||||
|
||||
|
||||
def operations_plan(db, identities, agents, kbs, business, models):
|
||||
if db.execute("PRAGMA query_only").fetchone()[0] != 1:
|
||||
raise ValueError("Operations import requires a read-only consistent snapshot")
|
||||
identities, agents, kbs = (_mapping(m) for m in (identities, agents, kbs))
|
||||
if identities.get("1") != ADMIN_TARGET_ID:
|
||||
raise ValueError("Pinned administrator identity differs")
|
||||
sources = {table: _read(db, table) for table in KINDS}
|
||||
campaigns = {r["id"]: r["owner_user_id"] for r in _read(db, "campaign")}
|
||||
segments = {r["id"]: r["campaign_id"] for r in _read(db, "segment")}
|
||||
balances = reconcile(sources["pointaccount"], sources["pointtransaction"])
|
||||
reconciliation = {r["source_user_id"]: r for r in balances}
|
||||
charges = defaultdict(list)
|
||||
for tx in sources["pointtransaction"]:
|
||||
event = tx["metadata"].get("usage_event_id")
|
||||
if event:
|
||||
charges[event].append(tx)
|
||||
plan = {"schema_version": 1, "source": SOURCE, "target_database": TARGET_DATABASE,
|
||||
"target_tenant_id": TARGET_TENANT, "policy": POLICY, "identity_mapping": identities,
|
||||
"agent_mapping": agents, "kb_mapping": kbs, "business_mapping": business,
|
||||
"model_mapping": models, "reconciliation": balances, "items": []}
|
||||
for kind, rows in sources.items():
|
||||
for source in rows:
|
||||
sid, review = source["id"], []
|
||||
owner = identities.get(str(source.get("user_id", source.get("owner_user_id", source.get("actor_user_id")))), "")
|
||||
timestamp = _time(source.get("created_at", source.get("used_at", source.get("updated_at"))))
|
||||
target = {"id": operation_id(kind, sid), "tenant_id": TARGET_TENANT, "created_at": timestamp}
|
||||
table = ""
|
||||
metadata = {"legacy_import": {"source_kind": kind, "source_id": sid, "read_only": True},
|
||||
"legacy_metadata": redact(source.get("metadata", {}))}
|
||||
if kind in ("pointaccount", "pointtransaction"):
|
||||
reconciliation_row = reconciliation[source["user_id"]]
|
||||
if not reconciliation_row["reconciled"]:
|
||||
review.append("balance_reconciliation_requires_admin_review")
|
||||
metadata["balance_reconciliation"] = reconciliation_row
|
||||
if not owner:
|
||||
review.append("missing_identity")
|
||||
elif kind == "pointaccount":
|
||||
table = "workbench_balances"
|
||||
target = {"tenant_id": TARGET_TENANT, "user_id": owner, "balance": source["balance_points"],
|
||||
"reserved": 0, "updated_at": _time(source["updated_at"])}
|
||||
else:
|
||||
table = "workbench_transactions"
|
||||
target.update(user_id=owner, delta=source["delta_points"], kind=source["kind"],
|
||||
reason=redact(source["reason"]), actor_id=identities.get(str(source["operator_user_id"]), ""),
|
||||
idempotency_key=f"legacy:pointtransaction:{sid}", metadata=metadata)
|
||||
elif kind == "usageevent":
|
||||
if not owner:
|
||||
review.append("missing_usage_owner")
|
||||
else:
|
||||
table = "workbench_usage"
|
||||
linked = charges[sid]
|
||||
if any(tx["user_id"] != source["user_id"] or tx["delta_points"] >= 0 for tx in linked):
|
||||
raise ValueError("Usage charge has conflicting owner or sign")
|
||||
points = -sum(tx["delta_points"] for tx in linked)
|
||||
metadata.update(legacy_unit=source["unit"], legacy_quantity=source["quantity"],
|
||||
legacy_currency=source["currency"], legacy_amount_minor=source["amount_minor"],
|
||||
legacy_transaction_ids=[tx["id"] for tx in linked],
|
||||
points_basis="exact_linked_debits" if linked else "no_linked_debit", legacy_call_id=source["llm_call_id"],
|
||||
legacy_campaign_id=source["campaign_id"], legacy_run_id=source["run_id"], legacy_model=redact(source["model"]))
|
||||
token = source["unit"] == "token"
|
||||
target.update(owner_id=owner, job_id="", call_id=f"legacy:usageevent:{sid}",
|
||||
agent_id=agents.get(str(source["agent_id"]), ""),
|
||||
model_id=models.get(f"{source['provider_id']}:{source['model']}", ""),
|
||||
action=redact(source["metadata"].get("operation") or source["service_type"]),
|
||||
input_tokens=source["input_quantity"] if token else 0,
|
||||
output_tokens=source["output_quantity"] if token else 0,
|
||||
points=points, status=source["status"], metadata=metadata)
|
||||
elif kind == "knowledgeusage":
|
||||
owner, reason = _relation_owner(source, campaigns, segments, identities)
|
||||
if reason:
|
||||
review.append(reason)
|
||||
if not agents.get(str(source["agent_id"])) or not kbs.get(str(source["kb_id"])):
|
||||
review.append("missing_consumption_asset_mapping")
|
||||
if not review:
|
||||
table = "workbench_consumption"
|
||||
# Reindexing changes chunks. Namespacing preserves historical usage
|
||||
# without falsely marking an unrelated native chunk consumed.
|
||||
chunk = "legacy:" + str(source["chunk_id"])
|
||||
if len(chunk) > 200:
|
||||
chunk = "legacy:sha256:" + hashlib.sha256(chunk.encode()).hexdigest()
|
||||
target.update(owner_id=owner, agent_id=agents[str(source["agent_id"])], kb_id=kbs[str(source["kb_id"])],
|
||||
chunk_id=chunk, knowledge_id="", file_name=redact(source["file_name"]),
|
||||
category=redact(source["heading"]), excerpt=redact(source["excerpt"]),
|
||||
resource_id=business.get(f"campaign:{source['campaign_id']}", ""),
|
||||
segment_id=business.get(f"segment:{source['segment_id']}", ""), job_id="",
|
||||
status=source["status"], banned=bool(source["banned"]), lease_until=None,
|
||||
updated_at=_time(source["released_at"] or source["used_at"]))
|
||||
elif kind == "notification":
|
||||
owner, reason = notification_owner(source, campaigns, segments, identities)
|
||||
if reason:
|
||||
review.append(reason)
|
||||
else:
|
||||
table = "workbench_events"
|
||||
target.update(owner_id=owner, resource_id="", action=source["type"], message=redact(source["message"]),
|
||||
level="error" if source["type"] == "failure" else "info", read=source["status"] == "read")
|
||||
elif kind == "pendingmemory":
|
||||
if not owner or not agents.get(str(source["target_agent_id"])):
|
||||
review.append("missing_memory_owner_or_agent")
|
||||
else:
|
||||
table = "workbench_pending_memories"
|
||||
target.update(owner_id=owner, agent_id=agents[str(source["target_agent_id"])], content=redact(source["content"]),
|
||||
source_ref=redact(source["source_ref"]), status={"approved": "accepted"}.get(source["status"], source["status"]),
|
||||
reviewed_by="", updated_at=timestamp)
|
||||
else:
|
||||
table = "workbench_operation_logs"
|
||||
system = kind == "systemlog"
|
||||
level = str(source["level"]).lower() if system else ("info" if source["result"] in ("ok", "success") else "error")
|
||||
status = source.get("status", 0) if system else (200 if level == "info" else 400)
|
||||
status = int(status) if str(status).isdigit() else 0
|
||||
target.update(owner_id=owner, owner_name=redact(source.get("actor_display_name", "")),
|
||||
kind="system" if system else "audit", level=level,
|
||||
action=redact(source["source"] if system else source["action"]),
|
||||
message=redact(source["message"] if system else source["result"]),
|
||||
resource_id=redact(source.get("resource", "")), trace_id=redact(source.get("trace_id", ""))[:100],
|
||||
status=status, metadata=metadata)
|
||||
plan["items"].append({"source_kind": kind, "source_id": sid, "source_snapshot": archive_text(source),
|
||||
"history_id": history_id(kind, sid), "owner_id": owner, "review": review,
|
||||
"target_table": table, "target_key": target.get("user_id") if table == "workbench_balances" else target["id"],
|
||||
"native_snapshot": target if table else {}, "created_at": timestamp,
|
||||
"history_payload": {"source": archive_text(redact(source)), "review": review,
|
||||
"read_only": True, "original_in_private_export": True}})
|
||||
if kind in ("pointaccount", "pointtransaction"):
|
||||
plan["items"][-1]["history_payload"]["balance_reconciliation"] = reconciliation[source["user_id"]]
|
||||
return plan
|
||||
|
||||
|
||||
def validate_plan(plan):
|
||||
if (plan.get("schema_version") != 1 or plan.get("source") != SOURCE or plan.get("target_database") != TARGET_DATABASE
|
||||
or plan.get("target_tenant_id") != TARGET_TENANT or plan.get("policy") != POLICY):
|
||||
raise ValueError("Operations migration scope differs")
|
||||
if _mapping(plan["identity_mapping"]).get("1") != ADMIN_TARGET_ID:
|
||||
raise ValueError("Operations administrator mapping differs")
|
||||
seen = set()
|
||||
for item in plan["items"]:
|
||||
key = item["source_kind"], item["source_id"]
|
||||
if key in seen or key[0] not in KINDS or item["source_snapshot"]["id"] != key[1] or item["history_id"] != history_id(*key):
|
||||
raise ValueError("Operations source identity conflict")
|
||||
seen.add(key)
|
||||
native, table = item["native_snapshot"], item["target_table"]
|
||||
if table and table != NATIVE_TABLE[key[0]]:
|
||||
raise ValueError("Operations native table differs")
|
||||
if table and native.get("tenant_id") != TARGET_TENANT:
|
||||
raise ValueError("Operations native tenant differs")
|
||||
if table and table != "workbench_balances" and native["id"] != operation_id(*key):
|
||||
raise ValueError("Operations native identity differs")
|
||||
if table and table != "workbench_operation_logs" and not item["owner_id"]:
|
||||
raise ValueError("Owned operations cannot be broadcast")
|
||||
if table:
|
||||
owner_column = "user_id" if table in ("workbench_balances", "workbench_transactions") else "owner_id"
|
||||
native_key = native["user_id"] if table == "workbench_balances" else native["id"]
|
||||
if native[owner_column] != item["owner_id"] or item["target_key"] != native_key:
|
||||
raise ValueError("Operations native owner or key differs")
|
||||
if item["owner_id"] and item["owner_id"] not in plan["identity_mapping"].values():
|
||||
raise ValueError("Operations owner is not mapped")
|
||||
accounts = [i["source_snapshot"] for i in plan["items"] if i["source_kind"] == "pointaccount"]
|
||||
transactions = [i["source_snapshot"] for i in plan["items"] if i["source_kind"] == "pointtransaction"]
|
||||
if plan["reconciliation"] != reconcile(accounts, transactions):
|
||||
raise ValueError("Balance reconciliation was changed")
|
||||
for item in plan["items"]:
|
||||
source, native = item["source_snapshot"], item["native_snapshot"]
|
||||
if item["target_table"] == "workbench_balances" and native["balance"] != source["balance_points"]:
|
||||
raise ValueError("Authoritative current balance must remain exact")
|
||||
if item["target_table"] == "workbench_transactions" and native["delta"] != source["delta_points"]:
|
||||
raise ValueError("Historical transactions must remain exact")
|
||||
|
||||
|
||||
def safe_operations_summary(plan):
|
||||
validate_plan(plan)
|
||||
return {"database": TARGET_DATABASE, "tenant_id": TARGET_TENANT,
|
||||
"source_counts": dict(Counter(i["source_kind"] for i in plan["items"])),
|
||||
"native_counts": dict(Counter(i["target_table"] for i in plan["items"] if i["target_table"])),
|
||||
"archive_only_counts": dict(Counter(i["source_kind"] for i in plan["items"] if not i["target_table"])),
|
||||
"review_counts": dict(Counter(r for i in plan["items"] for r in i["review"])),
|
||||
"reconciliation": plan["reconciliation"], "source_snapshot_sha256": plan.get("source_snapshot_sha256"),
|
||||
"history_owner_policy": "admin_only_archive", "full_original_export_required": True,
|
||||
"consumption_chunk_policy": "legacy_namespace_no_inferred_native_chunk", "jobs_created": 0}
|
||||
|
||||
|
||||
def operations_sql(plan):
|
||||
validate_plan(plan)
|
||||
fingerprint = plan.get("source_snapshot_sha256", "")
|
||||
if not re.fullmatch("[a-f0-9]{64}", fingerprint):
|
||||
raise ValueError("Operations source snapshot fingerprint required")
|
||||
literal = "'" + _canonical(plan).replace("'", "''") + "'::jsonb"
|
||||
sql = f"""SET LOCAL standard_conforming_strings=on;
|
||||
SET LOCAL log_statement='none'; SET LOCAL log_min_error_statement='panic';
|
||||
CREATE TEMP TABLE _ops_plan(payload jsonb) ON COMMIT DROP;
|
||||
INSERT INTO _ops_plan VALUES ({literal});
|
||||
DO $scope$ BEGIN
|
||||
IF current_database()<>'{TARGET_DATABASE}' THEN RAISE EXCEPTION 'Operations database mismatch'; END IF;
|
||||
PERFORM pg_advisory_xact_lock({TARGET_TENANT},11907);
|
||||
PERFORM 1 FROM tenants WHERE id={TARGET_TENANT} AND status='active' AND deleted_at IS NULL FOR SHARE;
|
||||
IF NOT FOUND THEN RAISE EXCEPTION 'Operations target tenant inactive'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_plan p,jsonb_each_text(p.payload->'identity_mapping') i
|
||||
LEFT JOIN {IDENTITY_LEDGER} m ON m.source_system='{SOURCE}' AND m.source_organization_id=1
|
||||
AND m.source_user_id=i.key::bigint AND m.target_tenant_id={TARGET_TENANT} AND m.target_user_id=i.value
|
||||
LEFT JOIN users u ON u.id=i.value AND u.deleted_at IS NULL
|
||||
LEFT JOIN tenant_members tm ON tm.user_id=i.value AND tm.tenant_id={TARGET_TENANT} AND tm.deleted_at IS NULL
|
||||
WHERE m.target_user_id IS NULL OR u.id IS NULL OR tm.id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations durable owner mapping conflict'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_plan p,jsonb_each_text(p.payload->'agent_mapping') i
|
||||
LEFT JOIN {ASSET_LEDGER} m ON m.source_system='{SOURCE}' AND m.source_organization_id=1
|
||||
AND m.source_kind='agent' AND m.source_id=i.key::bigint AND m.target_tenant_id={TARGET_TENANT} AND m.target_id=i.value
|
||||
LEFT JOIN custom_agents a ON a.id=i.value AND a.tenant_id={TARGET_TENANT}
|
||||
WHERE m.target_id IS NULL OR a.id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations durable Agent mapping conflict'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_plan p,jsonb_each_text(p.payload->'kb_mapping') i
|
||||
LEFT JOIN knowledge_bases k ON k.id=i.value AND k.tenant_id={TARGET_TENANT} AND k.deleted_at IS NULL
|
||||
WHERE k.id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations KB scope conflict'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_plan p,jsonb_each_text(p.payload->'business_mapping') i
|
||||
LEFT JOIN yawan_legacy_business_mappings b ON b.source_system='{SOURCE}' AND b.source_organization_id=1
|
||||
AND b.source_kind||':'||b.source_id=i.key AND b.target_tenant_id={TARGET_TENANT} AND b.target_id=i.value
|
||||
WHERE b.target_id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations durable business mapping conflict'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_plan p,jsonb_each_text(p.payload->'model_mapping') i
|
||||
LEFT JOIN yawan_legacy_model_mappings m ON m.source_system='{SOURCE}' AND m.source_organization_id=1
|
||||
AND m.target_tenant_id={TARGET_TENANT} AND m.target_model_id=i.value
|
||||
WHERE m.target_model_id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations durable model mapping conflict'; END IF;
|
||||
END $scope$;
|
||||
LOCK TABLE {','.join(TABLES)},workbench_history IN SHARE ROW EXCLUSIVE MODE;
|
||||
CREATE TABLE IF NOT EXISTS {LEDGER} (
|
||||
source_system text NOT NULL, source_organization_id bigint NOT NULL, source_kind text NOT NULL, source_id bigint NOT NULL,
|
||||
target_tenant_id bigint NOT NULL, history_id varchar(36) NOT NULL, target_table text NOT NULL, target_key text NOT NULL,
|
||||
source_snapshot jsonb NOT NULL, native_snapshot jsonb NOT NULL, review jsonb NOT NULL,
|
||||
source_snapshot_sha256 varchar(64) NOT NULL CHECK(source_snapshot_sha256 ~ '^[a-f0-9]{{64}}$'),
|
||||
created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(source_system,source_organization_id,source_kind,source_id),
|
||||
UNIQUE(target_tenant_id,history_id)
|
||||
);
|
||||
LOCK TABLE {LEDGER} IN SHARE ROW EXCLUSIVE MODE;
|
||||
CREATE TEMP TABLE _ops_items ON COMMIT DROP AS SELECT item.* FROM _ops_plan p,
|
||||
jsonb_to_recordset(p.payload->'items') AS item(source_kind text,source_id bigint,source_snapshot jsonb,
|
||||
history_id text,owner_id text,review jsonb,target_table text,target_key text,native_snapshot jsonb,created_at timestamptz,history_payload jsonb);
|
||||
CREATE INDEX ON _ops_items(source_kind,source_id);
|
||||
DO $provenance$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM _ops_items i JOIN {LEDGER} l USING(source_kind,source_id)
|
||||
WHERE l.source_system='{SOURCE}' AND l.source_organization_id=1 AND
|
||||
(l.target_tenant_id IS DISTINCT FROM {TARGET_TENANT} OR l.history_id IS DISTINCT FROM i.history_id
|
||||
OR l.target_table IS DISTINCT FROM i.target_table OR l.target_key IS DISTINCT FROM i.target_key
|
||||
OR l.source_snapshot IS DISTINCT FROM i.source_snapshot OR l.native_snapshot IS DISTINCT FROM i.native_snapshot
|
||||
OR l.review IS DISTINCT FROM i.review))
|
||||
THEN RAISE EXCEPTION 'Operations provenance conflict'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_items i JOIN workbench_history h ON h.id=i.history_id
|
||||
WHERE NOT EXISTS (SELECT 1 FROM {LEDGER} l WHERE l.source_system='{SOURCE}' AND l.source_organization_id=1
|
||||
AND l.source_kind=i.source_kind AND l.source_id=i.source_id))
|
||||
THEN RAISE EXCEPTION 'Operations history collision'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_items i JOIN {LEDGER} l USING(source_kind,source_id)
|
||||
LEFT JOIN workbench_history h ON h.id=i.history_id AND h.tenant_id={TARGET_TENANT} AND h.owner_id=''
|
||||
WHERE l.source_system='{SOURCE}' AND l.source_organization_id=1 AND h.id IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations history removed or scope changed'; END IF;
|
||||
END $provenance$;
|
||||
CREATE TEMP TABLE _ops_new ON COMMIT DROP AS SELECT i.* FROM _ops_items i WHERE NOT EXISTS
|
||||
(SELECT 1 FROM {LEDGER} l WHERE l.source_system='{SOURCE}' AND l.source_organization_id=1 AND l.source_kind=i.source_kind AND l.source_id=i.source_id);
|
||||
"""
|
||||
for table in TABLES:
|
||||
key = "user_id" if table == "workbench_balances" else "id"
|
||||
owner = "user_id" if table in ("workbench_balances", "workbench_transactions") else "owner_id"
|
||||
sql += f"""DO $native$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM _ops_new i JOIN {table} n ON n.{key}=i.target_key
|
||||
WHERE i.target_table='{table}' {'AND n.tenant_id=' + str(TARGET_TENANT) if key == 'user_id' else ''})
|
||||
THEN RAISE EXCEPTION 'Operations native collision in {table}'; END IF;
|
||||
IF EXISTS (SELECT 1 FROM _ops_items i JOIN {LEDGER} l USING(source_kind,source_id)
|
||||
LEFT JOIN {table} n ON n.{key}=i.target_key AND n.tenant_id={TARGET_TENANT} AND n.{owner}=i.owner_id
|
||||
WHERE l.source_system='{SOURCE}' AND l.source_organization_id=1 AND i.target_table='{table}' AND n.{key} IS NULL)
|
||||
THEN RAISE EXCEPTION 'Operations native row removed or ownership changed in {table}'; END IF;
|
||||
END $native$;
|
||||
INSERT INTO {table} SELECT (jsonb_populate_record(NULL::{table},i.native_snapshot)).*
|
||||
FROM _ops_new i WHERE i.target_table='{table}';
|
||||
"""
|
||||
sql += f"""INSERT INTO workbench_history(id,tenant_id,owner_id,source_kind,source_id,payload,created_at)
|
||||
SELECT history_id,{TARGET_TENANT},'',source_kind,source_id::text,history_payload,created_at FROM _ops_new;
|
||||
INSERT INTO {LEDGER}(source_system,source_organization_id,source_kind,source_id,target_tenant_id,history_id,target_table,target_key,
|
||||
source_snapshot,native_snapshot,review,source_snapshot_sha256)
|
||||
SELECT '{SOURCE}',1,source_kind,source_id,{TARGET_TENANT},history_id,target_table,target_key,source_snapshot,native_snapshot,review,'{fingerprint}' FROM _ops_new;
|
||||
SELECT json_build_object('inserted_history',(SELECT count(*) FROM _ops_new),
|
||||
'inserted_native',(SELECT count(*) FROM _ops_new WHERE target_table<>''),
|
||||
'ledger_total',(SELECT count(*) FROM {LEDGER} WHERE target_tenant_id={TARGET_TENANT}));
|
||||
"""
|
||||
return sql
|
||||
Reference in New Issue
Block a user