feat(db): migrate accounts/credentials/platforms to v2 schema
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,9 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from db.auth_strategy import ALL_AUTH_STRATEGIES, AUTH_STRATEGY_PASSWORD_AUTO
|
||||||
from db.connection import get_conn, init_db
|
from db.connection import get_conn, init_db
|
||||||
|
from db.platform_defaults import PLATFORM_DEFAULT_AUTH_STRATEGY
|
||||||
from util.logging_config import get_skill_logger
|
from util.logging_config import get_skill_logger
|
||||||
|
|
||||||
|
|
||||||
@@ -32,8 +34,30 @@ def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_UNSET = object()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_auth_strategy_for_platform(conn, platform_key: str) -> str:
|
||||||
|
"""Pick auth_strategy from platforms.default_auth_strategy, built-in map, or password_auto."""
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT default_auth_strategy FROM platforms WHERE platform_key = ?",
|
||||||
|
(platform_key,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
s = str(row[0])
|
||||||
|
if s in ALL_AUTH_STRATEGIES:
|
||||||
|
return s
|
||||||
|
mapped = PLATFORM_DEFAULT_AUTH_STRATEGY.get(platform_key)
|
||||||
|
if mapped and mapped in ALL_AUTH_STRATEGIES:
|
||||||
|
return mapped
|
||||||
|
return AUTH_STRATEGY_PASSWORD_AUTO
|
||||||
|
|
||||||
|
|
||||||
def _acc_row_to_dict(row) -> dict[str, Any]:
|
def _acc_row_to_dict(row) -> dict[str, Any]:
|
||||||
"""Convert an accounts table row tuple to a dict matching the v2 schema."""
|
"""Convert an accounts table row tuple to a dict matching the v2 schema."""
|
||||||
|
dev_fp = row[20]
|
||||||
return {
|
return {
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
"platform_key": row[1],
|
"platform_key": row[1],
|
||||||
@@ -53,8 +77,11 @@ def _acc_row_to_dict(row) -> dict[str, Any]:
|
|||||||
"last_error_code": row[15] or "",
|
"last_error_code": row[15] or "",
|
||||||
"last_error_message": row[16] or "",
|
"last_error_message": row[16] or "",
|
||||||
"extra_json": _safe_json_loads(row[17]),
|
"extra_json": _safe_json_loads(row[17]),
|
||||||
"created_at": _unix_to_iso_local(row[18]),
|
"auth_strategy": row[18],
|
||||||
"updated_at": _unix_to_iso_local(row[19]),
|
"session_persistent": int(row[19]) if row[19] is not None else 1,
|
||||||
|
"device_fingerprint": dev_fp if dev_fp is not None else None,
|
||||||
|
"created_at": _unix_to_iso_local(row[21]),
|
||||||
|
"updated_at": _unix_to_iso_local(row[22]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -74,14 +101,14 @@ def list_platforms_from_db(conn, domain: Optional[str] = None) -> list[dict]:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT platform_key, display_name, domain, provider_code, "
|
"SELECT platform_key, display_name, domain, provider_code, "
|
||||||
"default_url, aliases_json, capabilities_json, enabled, "
|
"default_url, aliases_json, capabilities_json, enabled, "
|
||||||
"created_at, updated_at FROM platforms WHERE domain = ? ORDER BY platform_key",
|
"default_auth_strategy, created_at, updated_at FROM platforms WHERE domain = ? ORDER BY platform_key",
|
||||||
(domain,),
|
(domain,),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT platform_key, display_name, domain, provider_code, "
|
"SELECT platform_key, display_name, domain, provider_code, "
|
||||||
"default_url, aliases_json, capabilities_json, enabled, "
|
"default_url, aliases_json, capabilities_json, enabled, "
|
||||||
"created_at, updated_at FROM platforms ORDER BY platform_key"
|
"default_auth_strategy, created_at, updated_at FROM platforms ORDER BY platform_key"
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
result = []
|
result = []
|
||||||
@@ -95,8 +122,9 @@ def list_platforms_from_db(conn, domain: Optional[str] = None) -> list[dict]:
|
|||||||
"aliases": _safe_json_loads(r[5], []),
|
"aliases": _safe_json_loads(r[5], []),
|
||||||
"capabilities": _safe_json_loads(r[6], {}),
|
"capabilities": _safe_json_loads(r[6], {}),
|
||||||
"enabled": bool(r[7]),
|
"enabled": bool(r[7]),
|
||||||
"created_at": _unix_to_iso_local(r[8]),
|
"default_auth_strategy": r[8],
|
||||||
"updated_at": _unix_to_iso_local(r[9]),
|
"created_at": _unix_to_iso_local(r[9]),
|
||||||
|
"updated_at": _unix_to_iso_local(r[10]),
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -106,7 +134,7 @@ def get_platform_from_db(conn, platform_key: str) -> Optional[dict]:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT platform_key, display_name, domain, provider_code, "
|
"SELECT platform_key, display_name, domain, provider_code, "
|
||||||
"default_url, aliases_json, capabilities_json, enabled, "
|
"default_url, aliases_json, capabilities_json, enabled, "
|
||||||
"created_at, updated_at FROM platforms WHERE platform_key = ?",
|
"default_auth_strategy, created_at, updated_at FROM platforms WHERE platform_key = ?",
|
||||||
(platform_key,),
|
(platform_key,),
|
||||||
)
|
)
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
@@ -121,8 +149,9 @@ def get_platform_from_db(conn, platform_key: str) -> Optional[dict]:
|
|||||||
"aliases": _safe_json_loads(r[5], []),
|
"aliases": _safe_json_loads(r[5], []),
|
||||||
"capabilities": _safe_json_loads(r[6], {}),
|
"capabilities": _safe_json_loads(r[6], {}),
|
||||||
"enabled": bool(r[7]),
|
"enabled": bool(r[7]),
|
||||||
"created_at": _unix_to_iso_local(r[8]),
|
"default_auth_strategy": r[8],
|
||||||
"updated_at": _unix_to_iso_local(r[9]),
|
"created_at": _unix_to_iso_local(r[9]),
|
||||||
|
"updated_at": _unix_to_iso_local(r[10]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -144,20 +173,30 @@ def insert_account(
|
|||||||
url: Optional[str],
|
url: Optional[str],
|
||||||
extra_json: Optional[str],
|
extra_json: Optional[str],
|
||||||
now: int,
|
now: int,
|
||||||
|
auth_strategy: str,
|
||||||
|
session_persistent: int = 1,
|
||||||
|
device_fingerprint: Optional[str] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
if auth_strategy not in ALL_AUTH_STRATEGIES:
|
||||||
|
raise ValueError(f"invalid auth_strategy: {auth_strategy!r}")
|
||||||
|
if session_persistent not in (0, 1):
|
||||||
|
raise ValueError("session_persistent must be 0 or 1")
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO accounts
|
INSERT INTO accounts
|
||||||
(platform_key, account_label, login_id, login_id_type, tenant_id,
|
(platform_key, account_label, login_id, login_id_type, tenant_id,
|
||||||
environment, role, provider_code, profile_dir, url,
|
environment, role, provider_code, profile_dir, url,
|
||||||
status, session_status, extra_json, created_at, updated_at)
|
status, session_status, extra_json,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'unknown', ?, ?, ?)
|
auth_strategy, session_persistent, device_fingerprint,
|
||||||
|
created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'unknown', ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
platform_key, account_label, login_id, login_id_type, tenant_id,
|
platform_key, account_label, login_id, login_id_type, tenant_id,
|
||||||
environment, role, provider_code, profile_dir, url,
|
environment, role, provider_code, profile_dir, url,
|
||||||
extra_json or "{}", now, now,
|
extra_json or "{}", auth_strategy, session_persistent, device_fingerprint,
|
||||||
|
now, now,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return int(cur.lastrowid)
|
return int(cur.lastrowid)
|
||||||
@@ -176,6 +215,7 @@ def get_account_by_id(account_id) -> Optional[dict]:
|
|||||||
profile_dir, url, status, session_status,
|
profile_dir, url, status, session_status,
|
||||||
last_used_at, last_login_check_at,
|
last_used_at, last_login_check_at,
|
||||||
last_error_code, last_error_message, extra_json,
|
last_error_code, last_error_message, extra_json,
|
||||||
|
auth_strategy, session_persistent, device_fingerprint,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
FROM accounts WHERE id = ?
|
FROM accounts WHERE id = ?
|
||||||
""",
|
""",
|
||||||
@@ -232,7 +272,9 @@ def find_accounts(
|
|||||||
"SELECT id, platform_key, account_label, login_id, login_id_type, "
|
"SELECT id, platform_key, account_label, login_id, login_id_type, "
|
||||||
"tenant_id, environment, role, provider_code, profile_dir, url, "
|
"tenant_id, environment, role, provider_code, profile_dir, url, "
|
||||||
"status, session_status, last_used_at, last_login_check_at, "
|
"status, session_status, last_used_at, last_login_check_at, "
|
||||||
"last_error_code, last_error_message, extra_json, created_at, updated_at "
|
"last_error_code, last_error_message, extra_json, "
|
||||||
|
"auth_strategy, session_persistent, device_fingerprint, "
|
||||||
|
"created_at, updated_at "
|
||||||
f"FROM accounts {where} ORDER BY updated_at DESC, id DESC LIMIT ?"
|
f"FROM accounts {where} ORDER BY updated_at DESC, id DESC LIMIT ?"
|
||||||
)
|
)
|
||||||
params.append(int(limit))
|
params.append(int(limit))
|
||||||
@@ -328,6 +370,42 @@ def update_account_error(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_account(
|
||||||
|
conn,
|
||||||
|
account_id: int,
|
||||||
|
*,
|
||||||
|
auth_strategy: Optional[str] = None,
|
||||||
|
session_persistent: Optional[int] = None,
|
||||||
|
device_fingerprint: Any = _UNSET,
|
||||||
|
) -> None:
|
||||||
|
"""Optional field updates for accounts v2 columns."""
|
||||||
|
sets: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
if auth_strategy is not None:
|
||||||
|
if auth_strategy not in ALL_AUTH_STRATEGIES:
|
||||||
|
raise ValueError(f"invalid auth_strategy: {auth_strategy!r}")
|
||||||
|
sets.append("auth_strategy = ?")
|
||||||
|
params.append(auth_strategy)
|
||||||
|
if session_persistent is not None:
|
||||||
|
if session_persistent not in (0, 1):
|
||||||
|
raise ValueError("session_persistent must be 0 or 1")
|
||||||
|
sets.append("session_persistent = ?")
|
||||||
|
params.append(session_persistent)
|
||||||
|
if device_fingerprint is not _UNSET:
|
||||||
|
sets.append("device_fingerprint = ?")
|
||||||
|
params.append(device_fingerprint)
|
||||||
|
if not sets:
|
||||||
|
return
|
||||||
|
sets.append("updated_at = ?")
|
||||||
|
params.append(_now_unix())
|
||||||
|
params.append(int(account_id))
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE accounts SET {', '.join(sets)} WHERE id = ?",
|
||||||
|
tuple(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def account_exists_with_login_id(conn, platform_key: str, login_id: str, environment: str, role: str) -> bool:
|
def account_exists_with_login_id(conn, platform_key: str, login_id: str, environment: str, role: str) -> bool:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import sqlite3
|
|||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from db.schema import ALL_DDL, SCHEMA_VERSION
|
from db.schema import SCHEMA_VERSION, ensure_schema
|
||||||
from util.constants import SKILL_SLUG
|
from util.constants import SKILL_SLUG
|
||||||
from util.logging_config import get_skill_logger
|
from util.logging_config import get_skill_logger
|
||||||
from util.runtime_paths import get_db_path
|
from util.runtime_paths import get_db_path
|
||||||
@@ -67,13 +67,6 @@ def _check_schema_version(conn: sqlite3.Connection) -> int:
|
|||||||
return int(row[0]) if row else 0
|
return int(row[0]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
def _create_new_tables(conn: sqlite3.Connection) -> None:
|
|
||||||
"""Execute ALL_DDL (all CREATE TABLE/INDEX IF NOT EXISTS)."""
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.executescript(ALL_DDL)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
"""Idempotent: migrate if needed, create tables, set schema version."""
|
"""Idempotent: migrate if needed, create tables, set schema version."""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
@@ -88,10 +81,14 @@ def init_db() -> None:
|
|||||||
backed_up = _backup_old_accounts(conn)
|
backed_up = _backup_old_accounts(conn)
|
||||||
if backed_up:
|
if backed_up:
|
||||||
log.info("schema_init_backup_done old_version=%s", old_version)
|
log.info("schema_init_backup_done old_version=%s", old_version)
|
||||||
_create_new_tables(conn)
|
|
||||||
cur = conn.cursor()
|
ensure_schema(conn)
|
||||||
cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
||||||
conn.commit()
|
cur = conn.cursor()
|
||||||
|
cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if old_version < SCHEMA_VERSION:
|
||||||
log.info(
|
log.info(
|
||||||
"schema_init_done version=%s migrated_from=%s",
|
"schema_init_done version=%s migrated_from=%s",
|
||||||
SCHEMA_VERSION,
|
SCHEMA_VERSION,
|
||||||
@@ -99,12 +96,6 @@ def init_db() -> None:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.info("schema_init_uptodate version=%s", SCHEMA_VERSION)
|
log.info("schema_init_uptodate version=%s", SCHEMA_VERSION)
|
||||||
|
|
||||||
# Ensure all tables exist even if schema version matches
|
|
||||||
_create_new_tables(conn)
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("schema_init_failure")
|
log.exception("schema_init_failure")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ Time stored as INTEGER Unix seconds (UTC). DDL comments are preserved in
|
|||||||
sqlite_master, visible in DBeaver / Navicat.
|
sqlite_master, visible in DBeaver / Navicat.
|
||||||
|
|
||||||
Schema version management:
|
Schema version management:
|
||||||
PRAGMA user_version = 2 (set in connection.py after migration).
|
- PRAGMA user_version = 2 (set in connection.py after ensure_schema).
|
||||||
|
- _schema_meta row schema_version=2 (written by migrate_v1_to_v2).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
from db.auth_strategy import AUTH_STRATEGY_PASSWORD_AUTO
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# platforms — Registry of all platforms (logistics, LLM, content, etc.)
|
# platforms — Registry of all platforms (logistics, LLM, content, etc.)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -24,6 +29,7 @@ PLATFORMS_TABLE_SQL = textwrap.dedent("""\
|
|||||||
aliases_json TEXT NOT NULL DEFAULT '[]',-- 别名 JSON 数组,中英文别名
|
aliases_json TEXT NOT NULL DEFAULT '[]',-- 别名 JSON 数组,中英文别名
|
||||||
capabilities_json TEXT NOT NULL DEFAULT '{}', -- 能力声明,例如 {"web":true,"api_key":true,"rpa":true}
|
capabilities_json TEXT NOT NULL DEFAULT '{}', -- 能力声明,例如 {"web":true,"api_key":true,"rpa":true}
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,-- 是否启用 (1=yes 0=no)
|
enabled INTEGER NOT NULL DEFAULT 1,-- 是否启用 (1=yes 0=no)
|
||||||
|
default_auth_strategy TEXT, -- 新账号默认认证策略;可空,手工维护
|
||||||
created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC
|
created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC
|
||||||
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
||||||
);
|
);
|
||||||
@@ -57,6 +63,9 @@ ACCOUNTS_TABLE_SQL = textwrap.dedent("""\
|
|||||||
last_error_code TEXT, -- 最近一次错误代码
|
last_error_code TEXT, -- 最近一次错误代码
|
||||||
last_error_message TEXT, -- 最近一次错误消息
|
last_error_message TEXT, -- 最近一次错误消息
|
||||||
extra_json TEXT NOT NULL DEFAULT '{}', -- 扩展字段 JSON
|
extra_json TEXT NOT NULL DEFAULT '{}', -- 扩展字段 JSON
|
||||||
|
auth_strategy TEXT NOT NULL, -- 认证策略(见 db.auth_strategy)
|
||||||
|
session_persistent INTEGER NOT NULL DEFAULT 1, -- 是否需要 profile 持久化 (0/1)
|
||||||
|
device_fingerprint TEXT, -- device_bound 策略用;可空
|
||||||
created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC
|
created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC
|
||||||
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
||||||
);
|
);
|
||||||
@@ -98,7 +107,6 @@ CREDENTIALS_TABLE_SQL = textwrap.dedent("""\
|
|||||||
|
|
||||||
CREDENTIALS_INDEXES_SQL = textwrap.dedent("""\
|
CREDENTIALS_INDEXES_SQL = textwrap.dedent("""\
|
||||||
CREATE INDEX IF NOT EXISTS idx_credentials_platform_type ON credentials(platform_key, credential_type);
|
CREATE INDEX IF NOT EXISTS idx_credentials_platform_type ON credentials(platform_key, credential_type);
|
||||||
CREATE INDEX IF NOT EXISTS idx_credentials_account ON credentials(account_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_credentials_status ON credentials(status);
|
CREATE INDEX IF NOT EXISTS idx_credentials_status ON credentials(status);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -128,6 +136,16 @@ ACCOUNT_LEASES_INDEXES_SQL = textwrap.dedent("""\
|
|||||||
CREATE INDEX IF NOT EXISTS idx_leases_expires ON account_leases(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_leases_expires ON account_leases(expires_at);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# schema meta — logical schema version (v1→v2 migration marker)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCHEMA_META_TABLE_SQL = textwrap.dedent("""\
|
||||||
|
CREATE TABLE IF NOT EXISTS _schema_meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Full DDL assembly
|
# Full DDL assembly
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -140,6 +158,112 @@ ALL_DDL = (
|
|||||||
+ CREDENTIALS_INDEXES_SQL
|
+ CREDENTIALS_INDEXES_SQL
|
||||||
+ ACCOUNT_LEASES_TABLE_SQL
|
+ ACCOUNT_LEASES_TABLE_SQL
|
||||||
+ ACCOUNT_LEASES_INDEXES_SQL
|
+ ACCOUNT_LEASES_INDEXES_SQL
|
||||||
|
+ SCHEMA_META_TABLE_SQL
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_VERSION = 2
|
SCHEMA_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
|
def column_exists(conn: sqlite3.Connection, table: str, col: str) -> bool:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(f"PRAGMA table_info({table})")
|
||||||
|
return any(row[1] == col for row in cur.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_meta_get(conn: sqlite3.Connection, key: str) -> str | None:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT value FROM _schema_meta WHERE key = ?", (key,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return str(row[0]) if row and row[0] is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_meta_upsert(conn: sqlite3.Connection, key: str, value: str) -> None:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO _schema_meta (key, value) VALUES (?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
||||||
|
""",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_v1_to_v2(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Apply ALTER steps + backfills once; idempotent via _schema_meta.schema_version."""
|
||||||
|
from db.platform_defaults import PLATFORM_DEFAULT_AUTH_STRATEGY
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
if _schema_meta_get(conn, "schema_version") == "2":
|
||||||
|
return
|
||||||
|
|
||||||
|
n_platform_updates = 0
|
||||||
|
|
||||||
|
# 1–2: platforms.default_auth_strategy
|
||||||
|
if not column_exists(conn, "platforms", "default_auth_strategy"):
|
||||||
|
cur.execute("ALTER TABLE platforms ADD COLUMN default_auth_strategy TEXT")
|
||||||
|
|
||||||
|
for pk, strat in PLATFORM_DEFAULT_AUTH_STRATEGY.items():
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE platforms SET default_auth_strategy = ? WHERE platform_key = ?",
|
||||||
|
(strat, pk),
|
||||||
|
)
|
||||||
|
n_platform_updates += cur.rowcount
|
||||||
|
|
||||||
|
# 3: accounts columns (nullable auth_strategy during ALTER for legacy rows)
|
||||||
|
if not column_exists(conn, "accounts", "auth_strategy"):
|
||||||
|
cur.execute("ALTER TABLE accounts ADD COLUMN auth_strategy TEXT")
|
||||||
|
if not column_exists(conn, "accounts", "session_persistent"):
|
||||||
|
cur.execute(
|
||||||
|
"ALTER TABLE accounts ADD COLUMN session_persistent INTEGER NOT NULL DEFAULT 1"
|
||||||
|
)
|
||||||
|
if not column_exists(conn, "accounts", "device_fingerprint"):
|
||||||
|
cur.execute("ALTER TABLE accounts ADD COLUMN device_fingerprint TEXT")
|
||||||
|
|
||||||
|
# 4: fill accounts.auth_strategy + session_persistent
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE accounts SET
|
||||||
|
auth_strategy = COALESCE(
|
||||||
|
(SELECT p.default_auth_strategy FROM platforms p
|
||||||
|
WHERE p.platform_key = accounts.platform_key),
|
||||||
|
?
|
||||||
|
),
|
||||||
|
session_persistent = COALESCE(session_persistent, 1)
|
||||||
|
WHERE auth_strategy IS NULL OR TRIM(auth_strategy) = ''
|
||||||
|
""",
|
||||||
|
(AUTH_STRATEGY_PASSWORD_AUTO,),
|
||||||
|
)
|
||||||
|
m_accounts_updated = cur.rowcount
|
||||||
|
|
||||||
|
# 5: credentials.account_id (v1 may omit it)
|
||||||
|
if not column_exists(conn, "credentials", "account_id"):
|
||||||
|
cur.execute("ALTER TABLE credentials ADD COLUMN account_id INTEGER")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_credentials_account ON credentials(account_id)"
|
||||||
|
)
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM platforms WHERE default_auth_strategy IS NULL")
|
||||||
|
k_null_platforms = int(cur.fetchone()[0])
|
||||||
|
|
||||||
|
_schema_meta_upsert(conn, "schema_version", "2")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[migration] account-manager v1→v2: updated {n_platform_updates} platforms, "
|
||||||
|
f"{m_accounts_updated} accounts.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"[migration] platforms with default_auth_strategy=NULL: "
|
||||||
|
f"{k_null_platforms} (manually fix later)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_schema(conn: sqlite3.Connection) -> None:
|
||||||
|
"""CREATE TABLE/INDEX IF NOT EXISTS, then run migrate_v1_to_v2."""
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.executescript(ALL_DDL)
|
||||||
|
conn.commit()
|
||||||
|
migrate_v1_to_v2(conn)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from db.accounts_repo import (
|
|||||||
list_platforms_from_db,
|
list_platforms_from_db,
|
||||||
platform_exists,
|
platform_exists,
|
||||||
release_lease,
|
release_lease,
|
||||||
|
resolve_auth_strategy_for_platform,
|
||||||
update_account_error,
|
update_account_error,
|
||||||
update_account_last_used,
|
update_account_last_used,
|
||||||
update_account_session_status,
|
update_account_session_status,
|
||||||
@@ -108,6 +109,7 @@ def _insert_secret_holder_account(
|
|||||||
url=url,
|
url=url,
|
||||||
extra_json="{}",
|
extra_json="{}",
|
||||||
now=now,
|
now=now,
|
||||||
|
auth_strategy=resolve_auth_strategy_for_platform(conn, platform_key),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -261,6 +263,7 @@ def cmd_account_add_web(
|
|||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
seed_platforms(conn)
|
seed_platforms(conn)
|
||||||
|
resolved_auth = resolve_auth_strategy_for_platform(conn, key)
|
||||||
new_id = insert_account(
|
new_id = insert_account(
|
||||||
conn=conn,
|
conn=conn,
|
||||||
platform_key=key,
|
platform_key=key,
|
||||||
@@ -275,6 +278,7 @@ def cmd_account_add_web(
|
|||||||
url=resolved_url,
|
url=resolved_url,
|
||||||
extra_json=resolved_extra,
|
extra_json=resolved_extra,
|
||||||
now=now,
|
now=now,
|
||||||
|
auth_strategy=resolved_auth,
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Reference in New Issue
Block a user