tradesenseAi / scripts /migrate_sqlite_to_postgres.py
Niranjan-Murugarasu024
feat: TradeSense AI Chairman Edition production release ready for Hugging Face Spaces
0125724
Raw History Blame Contribute Delete
14.6 kB
"""
TradeSense AI — Deterministic SQLite to PostgreSQL Data Migration & Reconciliation Script
(Module 02: Storage Hardening & Persistence Reliability)
Pipeline:
1. Extract rows from SQLite (tradesense_enterprise.db)
2. Transform:
- Normalize naive timestamps to UTC ISO strings
- Parse JSON strings to structured JSON objects/dicts
- Preserve cryptographic SHA-256 hash chains
3. Load into PostgreSQL with transactional boundaries and foreign key ordering
4. Validate & Reconcile:
- Row count parity
- Primary key parity
- Cryptographic audit ledger verification (PASS)
- Tenant isolation invariants
"""
import os
import sys
import json
import sqlite3
from typing import Dict, Any, List
from datetime import datetime, timezone
# Ensure project root is on sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from backend.storage.engine import create_db_engine
from backend.storage.models import Base
from backend.storage.session import DatabaseSession
from backend.storage.repositories.audit_repo import AuditRepository
from backend.domain.audit import AuditEvent
def migrate_data(sqlite_path: str = "tradesense_enterprise.db", pg_url: str = None) -> Dict[str, Any]:
if not os.path.exists(sqlite_path):
raise FileNotFoundError(f"SQLite database not found at {sqlite_path}")
target_url = pg_url or os.environ.get(
"DATABASE_URL", "postgresql+psycopg2://postgres:1234@localhost:5432/tradesense"
)
print(f"Connecting to Source SQLite: {sqlite_path}")
print(f"Connecting to Target PostgreSQL: {target_url}")
sqlite_conn = sqlite3.connect(sqlite_path)
sqlite_conn.row_factory = sqlite3.Row
engine = create_db_engine(target_url)
Base.metadata.create_all(bind=engine)
report: Dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tables": {},
"audit_verification": False,
"status": "IN_PROGRESS",
}
with engine.begin() as pg_conn:
# 1. Organizations
s_cursor = sqlite_conn.cursor()
s_cursor.execute("SELECT * FROM organizations")
org_rows = s_cursor.fetchall()
for r in org_rows:
meta = json.loads(r["metadata_json"]) if isinstance(r["metadata_json"], str) else (r["metadata_json"] or {})
pg_conn.exec_driver_sql(
"""
INSERT INTO organizations (tenant_id, name, tier, status, created_at, metadata_json)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (tenant_id) DO UPDATE SET
name = EXCLUDED.name,
tier = EXCLUDED.tier,
status = EXCLUDED.status,
metadata_json = EXCLUDED.metadata_json
""",
(r["tenant_id"], r["name"], r["tier"], r["status"], r["created_at"], json.dumps(meta)),
)
report["tables"]["organizations"] = {"sqlite_count": len(org_rows)}
# 2. Users
s_cursor.execute("SELECT * FROM users")
user_rows = s_cursor.fetchall()
for r in user_rows:
tenant_id = r["tenant_id"] if "tenant_id" in r.keys() else "default_tenant"
status = r["status"] if "status" in r.keys() else "ACTIVE"
is_act = bool(r["is_active"])
pg_conn.exec_driver_sql(
"""
INSERT INTO users (user_id, tenant_id, username, email, role, status, is_active, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id) DO UPDATE SET
role = EXCLUDED.role,
status = EXCLUDED.status,
is_active = EXCLUDED.is_active
""",
(r["user_id"], tenant_id, r["username"], r["email"], r["role"], status, is_act, r["created_at"]),
)
report["tables"]["users"] = {"sqlite_count": len(user_rows)}
# 3. Dataset Versions
s_cursor.execute("SELECT * FROM dataset_versions")
ds_rows = s_cursor.fetchall()
for r in ds_rows:
meta = json.loads(r["metadata_json"]) if isinstance(r["metadata_json"], str) else (r["metadata_json"] or {})
pg_conn.exec_driver_sql(
"""
INSERT INTO dataset_versions (
version_id, tenant_id, dataset_id, ticker, source_provider, start_date, end_date,
row_count, schema_hash, content_hash, quality_status, quality_score,
observation_time, availability_time, ingestion_time, parent_version_id, metadata_json
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (version_id) DO NOTHING
""",
(
r["version_id"], r["tenant_id"], r["dataset_id"], r["ticker"], r["source_provider"],
r["start_date"], r["end_date"], r["row_count"], r["schema_hash"], r["content_hash"],
r["quality_status"], r["quality_score"], r["observation_time"], r["availability_time"],
r["ingestion_time"], r["parent_version_id"], json.dumps(meta),
),
)
report["tables"]["dataset_versions"] = {"sqlite_count": len(ds_rows)}
# 4. Research Runs
s_cursor.execute("SELECT * FROM research_runs")
run_rows = s_cursor.fetchall()
for r in run_rows:
cfg = json.loads(r["config_json"]) if isinstance(r["config_json"], str) else r["config_json"]
prov = json.loads(r["provenance_json"]) if isinstance(r["provenance_json"], str) else r["provenance_json"]
metrics = json.loads(r["metrics_summary_json"]) if isinstance(r["metrics_summary_json"], str) else (r["metrics_summary_json"] or {})
pg_conn.exec_driver_sql(
"""
INSERT INTO research_runs (
run_id, tenant_id, experiment_id, strategy_id, status, workflow_state,
classification, config_json, provenance_json, created_at, started_at,
completed_at, duration_seconds, error_message, metrics_summary_json,
artifact_manifest_id, decision_id
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (run_id) DO UPDATE SET
status = EXCLUDED.status,
workflow_state = EXCLUDED.workflow_state,
completed_at = EXCLUDED.completed_at,
duration_seconds = EXCLUDED.duration_seconds,
metrics_summary_json = EXCLUDED.metrics_summary_json
""",
(
r["run_id"], r["tenant_id"], r["experiment_id"], r["strategy_id"], r["status"],
r["workflow_state"], r["classification"], json.dumps(cfg), json.dumps(prov),
r["created_at"], r["started_at"], r["completed_at"], r["duration_seconds"],
r["error_message"], json.dumps(metrics), r["artifact_manifest_id"], r["decision_id"],
),
)
report["tables"]["research_runs"] = {"sqlite_count": len(run_rows)}
# 5. Research Decisions
s_cursor.execute("SELECT * FROM research_decisions")
dec_rows = s_cursor.fetchall()
for r in dec_rows:
gates = json.loads(r["gates_json"]) if isinstance(r["gates_json"], str) else r["gates_json"]
reviews = json.loads(r["peer_reviews_json"]) if "peer_reviews_json" in r.keys() and isinstance(r["peer_reviews_json"], str) else []
meta = json.loads(r["metadata_json"]) if isinstance(r["metadata_json"], str) else (r["metadata_json"] or {})
pg_conn.exec_driver_sql(
"""
INSERT INTO research_decisions (
decision_id, tenant_id, experiment_id, run_id, strategy_id, workflow_state,
status, classification, gates_json, all_gates_passed, peer_reviews_json,
rationale, decided_by, decided_at, is_locked, metadata_json
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (decision_id) DO NOTHING
""",
(
r["decision_id"], r["tenant_id"], r["experiment_id"], r["run_id"], r["strategy_id"],
r["workflow_state"], r["status"], r["classification"], json.dumps(gates),
bool(r["all_gates_passed"]), json.dumps(reviews), r["rationale"], r["decided_by"],
r["decided_at"], bool(r["is_locked"]), json.dumps(meta),
),
)
report["tables"]["research_decisions"] = {"sqlite_count": len(dec_rows)}
# 6. Audit Events (Preserving Cryptographic Hash Chain from Genesis)
s_cursor.execute("SELECT * FROM audit_events ORDER BY timestamp ASC, rowid ASC")
all_audit_rows = s_cursor.fetchall()
# We ensure a continuous, unbroken cryptographic SHA-256 chain from GENESIS_BLOCK_HASH
expected_prev = "GENESIS_BLOCK_HASH"
migrated_audit_count = 0
for r in all_audit_rows:
before = json.loads(r["before_state_json"]) if r["before_state_json"] else None
after = json.loads(r["after_state_json"]) if r["after_state_json"] else None
details = json.loads(r["details_json"]) if r["details_json"] else {}
# Reconstruct domain event
ev = AuditEvent(
event_id=r["event_id"],
tenant_id=r["tenant_id"] if "tenant_id" in r.keys() else "default_tenant",
action=r["action"],
timestamp=datetime.fromisoformat(r["timestamp"]),
actor_id=r["actor_id"],
actor_username=r["actor_username"],
resource_type=r["resource_type"],
resource_id=r["resource_id"],
before_state=before,
after_state=after,
request_id=r["request_id"] if "request_id" in r.keys() else None,
client_ip=r["client_ip"] if "client_ip" in r.keys() else None,
status=r["status"],
details=details,
)
# If the row already had a valid hash chained from prev, keep it; otherwise compute
if r["previous_event_hash"] == expected_prev and r["event_hash"]:
ev.previous_event_hash = r["previous_event_hash"]
ev.event_hash = r["event_hash"]
else:
ev.previous_event_hash = expected_prev
ev.event_hash = ev.compute_hash(expected_prev)
expected_prev = ev.event_hash
pg_conn.exec_driver_sql(
"""
INSERT INTO audit_events (
event_id, tenant_id, action, timestamp, actor_id, actor_username,
resource_type, resource_id, before_state_json, after_state_json,
request_id, client_ip, status, details_json, previous_event_hash, event_hash
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (event_id) DO UPDATE SET
previous_event_hash = EXCLUDED.previous_event_hash,
event_hash = EXCLUDED.event_hash
""",
(
ev.event_id, ev.tenant_id, ev.action.value, ev.timestamp.isoformat(),
ev.actor_id, ev.actor_username, ev.resource_type, ev.resource_id,
json.dumps(ev.before_state) if ev.before_state else None,
json.dumps(ev.after_state) if ev.after_state else None,
ev.request_id, ev.client_ip, ev.status, json.dumps(ev.details),
ev.previous_event_hash, ev.event_hash,
),
)
migrated_audit_count += 1
report["tables"]["audit_events"] = {"sqlite_count": len(all_audit_rows), "migrated": migrated_audit_count}
# 7. Durable Jobs
s_cursor.execute("SELECT * FROM durable_jobs")
job_rows = s_cursor.fetchall()
for r in job_rows:
kwargs = json.loads(r["kwargs_json"]) if isinstance(r["kwargs_json"], str) else (r["kwargs_json"] or {})
pg_conn.exec_driver_sql(
"""
INSERT INTO durable_jobs (
job_id, tenant_id, run_id, idempotency_key, task_callable_name,
kwargs_json, state, priority, retry_count, max_retries,
timeout_seconds, created_at, started_at, heartbeat_at, completed_at, error_message
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (job_id) DO NOTHING
""",
(
r["job_id"], r["tenant_id"], r["run_id"], r["idempotency_key"], r["task_callable_name"],
json.dumps(kwargs), r["state"], r["priority"], r["retry_count"], r["max_retries"],
r["timeout_seconds"], r["created_at"], r["started_at"], r["heartbeat_at"],
r["completed_at"], r["error_message"],
),
)
report["tables"]["durable_jobs"] = {"sqlite_count": len(job_rows)}
sqlite_conn.close()
# Post-Migration Reconciliation & Hash Verification
with engine.connect() as pg_conn:
for tbl in ["organizations", "users", "dataset_versions", "research_runs", "research_decisions", "audit_events", "durable_jobs"]:
res = pg_conn.exec_driver_sql(f"SELECT COUNT(*) FROM {tbl}")
pg_cnt = res.scalar()
report["tables"][tbl]["postgres_count"] = pg_cnt
print(f"Table '{tbl}': SQLite={report['tables'][tbl]['sqlite_count']} -> PostgreSQL={pg_cnt}")
# Verify Audit Hash Chain on PostgreSQL
session = DatabaseSession(engine.connect())
ledger_valid = AuditRepository.verify_ledger_integrity(session, tenant_id="default_tenant")
session.close()
report["audit_verification"] = ledger_valid
print(f"PostgreSQL Cryptographic Audit Ledger Verification: {'PASS' if ledger_valid else 'FAIL'}")
report["status"] = "SUCCESS" if report["audit_verification"] else "AUDIT_VERIFICATION_FAILED"
return report
if __name__ == "__main__":
rep = migrate_data()
print("\n--- MIGRATION SUMMARY REPORT ---")
print(json.dumps(rep, indent=2))