oclaw/runtime/operations/scripts/cutover_sqlite_to_postgresql.ps1
oliver d14e9d3596 feat(persistence): PostgreSQL assistant store, chat persist fixes, gateway scripts
- Add SQLAlchemy Core repos, pg adapter/compat, assistant_store factory, Alembic bootstrap and migration/cutover scripts.

- Harden chat_message writes (NUL scrub for PG), turn_uuid on attempt failure, WS turn_runner fallbacks and gateway executed_turn_uuid init.

- start_gateway: log paths, PS7 stderr handling via cmd, background stdout/stderr redirect; runtime assistant_runtime_log_dir export.

- Ops: clear_all_chat_sessions with PG-only --postgresql and env-gated wipe; clear_postgres_chat_sessions.ps1.

- Tests: SA repos, pg compat, persist fallback, smoke env isolation; CI and docs touch-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:33:50 +08:00

128 lines
4.3 KiB
PowerShell

<#
.SYNOPSIS
Backup assistant SQLite DB, optionally dry-run, then import into PostgreSQL (empty schema).
.DESCRIPTION
1) Resolves SQLite path (explicit -SqlitePath or via Python db_path() / AIA_ASSISTANT_DB_PATH).
2) Copies the file to data/pg_cutover_backups/ (or -BackupDir).
3) Runs migrate_assistant_sqlite_to_postgresql.py --dry-run unless -SkipDryRun.
4) Runs the same script without --dry-run unless -DryRunOnly.
Set -PgUrl and/or ensure AIA_ASSISTANT_DATABASE_URL is set; with -LoadSystemEnv, URL may come only from _local/system.env (then -PgUrl can be omitted).
.EXAMPLE
.\cutover_sqlite_to_postgresql.ps1 -PgUrl "postgresql+psycopg://postgres:PASS@127.0.0.1:5432/oclaw"
.EXAMPLE
$env:AIA_ASSISTANT_DATABASE_URL = "postgresql+psycopg://..."
.\cutover_sqlite_to_postgresql.ps1
.EXAMPLE
.\cutover_sqlite_to_postgresql.ps1 -SqlitePath "D:\data\ai_ops.sqlite" -PgUrl "postgresql://..." -DryRunOnly
#>
[CmdletBinding()]
param(
[string] $PgUrl = "",
[string] $SqlitePath = "",
[string] $BackupDir = "",
[switch] $LoadSystemEnv,
[switch] $SkipDryRun,
[switch] $DryRunOnly,
[switch] $NoBackup
)
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
Set-Location $RepoRoot
if ($LoadSystemEnv) {
$envFile = Join-Path $RepoRoot "_local\system.env"
if (Test-Path -LiteralPath $envFile) {
Get-Content -LiteralPath $envFile | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith("#")) { return }
$i = $line.IndexOf("=")
if ($i -lt 1) { return }
$k = $line.Substring(0, $i).Trim()
$v = $line.Substring($i + 1).Trim()
if ($k) { Set-Item -Path "Env:$k" -Value $v }
}
Write-Host "Loaded _local/system.env into process environment."
}
else {
Write-Warning "LoadSystemEnv specified but $envFile not found."
}
}
if (-not $PgUrl) {
$PgUrl = $env:AIA_ASSISTANT_DATABASE_URL
}
if (-not $PgUrl -and -not $LoadSystemEnv) {
throw "Provide -PgUrl, set AIA_ASSISTANT_DATABASE_URL, or use -LoadSystemEnv (PostgreSQL URL in _local/system.env)."
}
if (-not $SqlitePath) {
$env:_OC_REPO_ROOT_FOR_PY = $RepoRoot
try {
$SqlitePath = (& python -c "import os,sys; sys.path.insert(0, os.environ['_OC_REPO_ROOT_FOR_PY']); from svc.config.paths import db_path; print(db_path(), end='')").Trim()
}
finally {
Remove-Item Env:_OC_REPO_ROOT_FOR_PY -ErrorAction SilentlyContinue
}
if (-not $SqlitePath) { throw "Could not resolve SQLite path via db_path()." }
Write-Host "Resolved SQLite: $SqlitePath"
}
if (-not (Test-Path -LiteralPath $SqlitePath)) {
throw "SQLite file not found: $SqlitePath"
}
$resolvedSqlite = (Resolve-Path -LiteralPath $SqlitePath).Path
$migrate = Join-Path $RepoRoot "runtime\operations\scripts\migrate_assistant_sqlite_to_postgresql.py"
if (-not (Test-Path -LiteralPath $migrate)) {
throw "Migration script not found: $migrate"
}
if (-not $BackupDir) {
$BackupDir = Join-Path $RepoRoot "data\pg_cutover_backups"
}
New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$leaf = [System.IO.Path]::GetFileNameWithoutExtension($resolvedSqlite)
$bakName = "${leaf}_pre_pg_${stamp}.sqlite"
$bakPath = Join-Path $BackupDir $bakName
if (-not $NoBackup) {
Copy-Item -LiteralPath $resolvedSqlite -Destination $bakPath -Force
Write-Host "Backup written: $bakPath"
}
else {
Write-Warning "NoBackup: skipping file copy (no SQLite backup created)."
}
$common = @($migrate)
if ($LoadSystemEnv) {
$common += "--load-system-env"
}
$common += "--sqlite", $resolvedSqlite
if ($PgUrl) {
$common += @("--pg-url", $PgUrl)
}
if (-not $SkipDryRun) {
Write-Host "=== Dry-run (row counts, no PG writes) ===" -ForegroundColor Cyan
& python @common "--dry-run"
if ($LASTEXITCODE -ne 0) { throw "Dry-run failed (exit $LASTEXITCODE)." }
}
if ($DryRunOnly) {
Write-Host "DryRunOnly: skipping live import." -ForegroundColor Yellow
exit 0
}
Write-Host "=== Live import into PostgreSQL ===" -ForegroundColor Cyan
& python @common
if ($LASTEXITCODE -ne 0) { throw "Migration failed (exit $LASTEXITCODE)." }
Write-Host ""
Write-Host "Done. Next: set AIA_ASSISTANT_DB_BACKEND=postgresql and AIA_ASSISTANT_DATABASE_URL in deployment, restart gateway." -ForegroundColor Green