Compare commits
2 Commits
9d5befdde9
...
0ef3e3bb7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ef3e3bb7a | |||
| ea38528ecf |
|
|
@ -339,7 +339,7 @@ def list_conversations_for_day(profile_id: str, journal_day_id: str) -> list[dic
|
||||||
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count
|
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count
|
||||||
FROM conversations c
|
FROM conversations c
|
||||||
WHERE c.profile_id = ? AND c.journal_day_id = ?
|
WHERE c.profile_id = ? AND c.journal_day_id = ?
|
||||||
ORDER BY c.created, c.rowid
|
ORDER BY c.created, c.id
|
||||||
""",
|
""",
|
||||||
(profile_id, journal_day_id),
|
(profile_id, journal_day_id),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ def current_entries(profile_id: str, journal_day_id: str) -> list[dict]:
|
||||||
FROM journal_entries e
|
FROM journal_entries e
|
||||||
LEFT JOIN journal_entry_versions v ON v.id = e.current_version_id
|
LEFT JOIN journal_entry_versions v ON v.id = e.current_version_id
|
||||||
WHERE e.profile_id = ? AND e.journal_day_id = ? AND e.deleted_at IS NULL
|
WHERE e.profile_id = ? AND e.journal_day_id = ? AND e.deleted_at IS NULL
|
||||||
ORDER BY e.created, e.rowid
|
ORDER BY e.created, e.id
|
||||||
""",
|
""",
|
||||||
(profile_id, journal_day_id),
|
(profile_id, journal_day_id),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
@ -514,7 +514,7 @@ def list_space_entries(profile_id: str, space_id: str) -> list[dict]:
|
||||||
JOIN journal_days d ON d.id = e.journal_day_id
|
JOIN journal_days d ON d.id = e.journal_day_id
|
||||||
LEFT JOIN journal_entry_versions v ON v.id = e.current_version_id
|
LEFT JOIN journal_entry_versions v ON v.id = e.current_version_id
|
||||||
WHERE e.profile_id = ? AND e.space_id = ? AND e.deleted_at IS NULL
|
WHERE e.profile_id = ? AND e.space_id = ? AND e.deleted_at IS NULL
|
||||||
ORDER BY d.calendar_date DESC, e.created ASC, e.rowid ASC
|
ORDER BY d.calendar_date DESC, e.created ASC, e.id ASC
|
||||||
""",
|
""",
|
||||||
(profile_id, space_id),
|
(profile_id, space_id),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ def _day_messages(profile_id: str, spec: dict[str, Any]) -> list[dict]:
|
||||||
FROM messages m
|
FROM messages m
|
||||||
JOIN conversations c ON c.id = m.conversation_id
|
JOIN conversations c ON c.id = m.conversation_id
|
||||||
WHERE m.profile_id = ? AND c.journal_day_id = ? AND m.conversation_id IN ({placeholders})
|
WHERE m.profile_id = ? AND c.journal_day_id = ? AND m.conversation_id IN ({placeholders})
|
||||||
ORDER BY c.created, c.rowid, m.seq, m.id
|
ORDER BY c.created, c.id, m.seq, m.id
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(profile_id, journal_day_id, *conversation_ids, fetch_limit),
|
(profile_id, journal_day_id, *conversation_ids, fetch_limit),
|
||||||
|
|
@ -107,7 +107,7 @@ def _day_messages(profile_id: str, spec: dict[str, Any]) -> list[dict]:
|
||||||
FROM messages m
|
FROM messages m
|
||||||
JOIN conversations c ON c.id = m.conversation_id
|
JOIN conversations c ON c.id = m.conversation_id
|
||||||
WHERE m.profile_id = ? AND c.journal_day_id = ?
|
WHERE m.profile_id = ? AND c.journal_day_id = ?
|
||||||
ORDER BY c.created, c.rowid, m.seq, m.id
|
ORDER BY c.created, c.id, m.seq, m.id
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(profile_id, journal_day_id, fetch_limit),
|
(profile_id, journal_day_id, fetch_limit),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
_INSERT_OR_IGNORE = re.compile(r"INSERT\s+OR\s+IGNORE\s+INTO", re.IGNORECASE)
|
_INSERT_OR_IGNORE = re.compile(r"INSERT\s+OR\s+IGNORE\s+INTO", re.IGNORECASE)
|
||||||
|
_ROWID_COL = re.compile(r"\.rowid\b", re.IGNORECASE)
|
||||||
_ON_CONFLICT_PAREN = re.compile(r"ON\s+CONFLICT\s*\(", re.IGNORECASE)
|
_ON_CONFLICT_PAREN = re.compile(r"ON\s+CONFLICT\s*\(", re.IGNORECASE)
|
||||||
_PRAGMA_TABLE = re.compile(
|
_PRAGMA_TABLE = re.compile(
|
||||||
r"^\s*PRAGMA\s+table_info\(\s*['\"]?(\w+)['\"]?\s*\)\s*;?\s*$",
|
r"^\s*PRAGMA\s+table_info\(\s*['\"]?(\w+)['\"]?\s*\)\s*;?\s*$",
|
||||||
|
|
@ -101,6 +102,7 @@ def replace_placeholders(sql: str) -> str:
|
||||||
def adapt_sql(sql: str) -> str:
|
def adapt_sql(sql: str) -> str:
|
||||||
"""Make a SQLite-shaped statement runnable on PostgreSQL."""
|
"""Make a SQLite-shaped statement runnable on PostgreSQL."""
|
||||||
sql = sqlite_schema_to_postgres(sql)
|
sql = sqlite_schema_to_postgres(sql)
|
||||||
|
sql = _ROWID_COL.sub(".id", sql)
|
||||||
sql = _ON_CONFLICT_PAREN.sub("ON CONFLICT (", sql)
|
sql = _ON_CONFLICT_PAREN.sub("ON CONFLICT (", sql)
|
||||||
used_ignore = bool(_INSERT_OR_IGNORE.search(sql))
|
used_ignore = bool(_INSERT_OR_IGNORE.search(sql))
|
||||||
sql = _INSERT_OR_IGNORE.sub("INSERT INTO", sql)
|
sql = _INSERT_OR_IGNORE.sub("INSERT INTO", sql)
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,18 @@ def migrate(sqlite_path: Path, media_src: Path, media_dest: Path, replace: bool)
|
||||||
stats: dict[str, tuple[int, int]] = {}
|
stats: dict[str, tuple[int, int]] = {}
|
||||||
try:
|
try:
|
||||||
existing = pg.execute("SELECT COUNT(*) FROM profiles").fetchone()[0]
|
existing = pg.execute("SELECT COUNT(*) FROM profiles").fetchone()[0]
|
||||||
|
seeds = pg.execute("SELECT COUNT(*) FROM tiers").fetchone()[0]
|
||||||
if existing and not replace:
|
if existing and not replace:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"PostgreSQL already has {existing} profiles. Pass --replace after a probe copy, not on the only backup."
|
f"PostgreSQL already has {existing} profiles. Pass --replace after a probe copy, not on the only backup."
|
||||||
)
|
)
|
||||||
if existing and replace:
|
if seeds and not replace:
|
||||||
print(f"Replacing {existing} existing Postgres profiles")
|
raise SystemExit(
|
||||||
|
"PostgreSQL already has platform seeds from startup. "
|
||||||
|
"Pass --replace to load a SQLite snapshot onto this instance (no user profiles yet is OK)."
|
||||||
|
)
|
||||||
|
if replace:
|
||||||
|
print(f"Replacing Postgres snapshot (profiles={existing}, seed_tiers={seeds})")
|
||||||
for table in reversed(TABLES):
|
for table in reversed(TABLES):
|
||||||
pg.execute(f"TRUNCATE TABLE {table} CASCADE")
|
pg.execute(f"TRUNCATE TABLE {table} CASCADE")
|
||||||
|
|
||||||
|
|
@ -179,6 +185,10 @@ def migrate(sqlite_path: Path, media_src: Path, media_dest: Path, replace: bool)
|
||||||
count = pg.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
count = pg.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
||||||
print(f" {table}: {len(rows)} → {count}")
|
print(f" {table}: {len(rows)} → {count}")
|
||||||
stats[table] = (len(rows), int(count))
|
stats[table] = (len(rows), int(count))
|
||||||
|
pg.execute(
|
||||||
|
"INSERT INTO schema_migrations (id) VALUES (%s) ON CONFLICT (id) DO NOTHING",
|
||||||
|
("022_postgres_runtime_baseline",),
|
||||||
|
)
|
||||||
pg.commit()
|
pg.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pg.rollback()
|
pg.rollback()
|
||||||
|
|
@ -195,6 +205,9 @@ def verify(stats: dict[str, tuple[int, int]]) -> None:
|
||||||
failed = False
|
failed = False
|
||||||
print("\nVerification")
|
print("\nVerification")
|
||||||
for table, (src, dest) in stats.items():
|
for table, (src, dest) in stats.items():
|
||||||
|
if table == "schema_migrations" and dest == src + 1:
|
||||||
|
print(f" ok {table:36} sqlite={src:5} postgres={dest:5} (incl. 022)")
|
||||||
|
continue
|
||||||
mark = "ok" if src == dest else "MISMATCH"
|
mark = "ok" if src == dest else "MISMATCH"
|
||||||
if src != dest:
|
if src != dest:
|
||||||
failed = True
|
failed = True
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ def main() -> None:
|
||||||
case_sql = adapt_sql("UPDATE t SET x = CASE WHEN ? = 1 THEN ? ELSE x END")
|
case_sql = adapt_sql("UPDATE t SET x = CASE WHEN ? = 1 THEN ? ELSE x END")
|
||||||
expect("CASE WHEN %s = 1 THEN %s ELSE x END" in case_sql, "boolean-safe case placeholder")
|
expect("CASE WHEN %s = 1 THEN %s ELSE x END" in case_sql, "boolean-safe case placeholder")
|
||||||
|
|
||||||
|
rowid_sql = adapt_sql("SELECT e.* FROM journal_entries e ORDER BY e.created, e.rowid")
|
||||||
|
expect("e.rowid" not in rowid_sql, "sqlite rowid removed for postgres")
|
||||||
|
expect("ORDER BY e.created, e.id" in rowid_sql, "rowid becomes primary id")
|
||||||
|
|
||||||
parts = split_sql("CREATE TABLE a (id TEXT); CREATE TABLE b (id TEXT);")
|
parts = split_sql("CREATE TABLE a (id TEXT); CREATE TABLE b (id TEXT);")
|
||||||
expect(parts == ["CREATE TABLE a (id TEXT)", "CREATE TABLE b (id TEXT)"], "split two statements")
|
expect(parts == ["CREATE TABLE a (id TEXT)", "CREATE TABLE b (id TEXT)"], "split two statements")
|
||||||
schema = (ROOT / "schema.sql").read_text(encoding="utf-8")
|
schema = (ROOT / "schema.sql").read_text(encoding="utf-8")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user