feat(db): migrate accounts/credentials/platforms to v2 schema
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,11 +6,16 @@ Time stored as INTEGER Unix seconds (UTC). DDL comments are preserved in
|
||||
sqlite_master, visible in DBeaver / Navicat.
|
||||
|
||||
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
|
||||
|
||||
from db.auth_strategy import AUTH_STRATEGY_PASSWORD_AUTO
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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 数组,中英文别名
|
||||
capabilities_json TEXT NOT NULL DEFAULT '{}', -- 能力声明,例如 {"web":true,"api_key":true,"rpa":true}
|
||||
enabled INTEGER NOT NULL DEFAULT 1,-- 是否启用 (1=yes 0=no)
|
||||
default_auth_strategy TEXT, -- 新账号默认认证策略;可空,手工维护
|
||||
created_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_message TEXT, -- 最近一次错误消息
|
||||
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
|
||||
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
||||
);
|
||||
@@ -98,7 +107,6 @@ CREDENTIALS_TABLE_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_account ON credentials(account_id);
|
||||
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);
|
||||
""")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -140,6 +158,112 @@ ALL_DDL = (
|
||||
+ CREDENTIALS_INDEXES_SQL
|
||||
+ ACCOUNT_LEASES_TABLE_SQL
|
||||
+ ACCOUNT_LEASES_INDEXES_SQL
|
||||
+ SCHEMA_META_TABLE_SQL
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user