add SQLite triggers to auto-fix Zotero date formats

Zotero writes timestamps as 'YYYY-MM-DD HH:MM:SS' internally but
the sync engine requires ISO 8601 'YYYY-MM-DDTHH:MM:SSZ'. Six
AFTER INSERT/UPDATE triggers on the items table auto-convert
dateAdded, dateModified, and clientDateModified on every write.

Includes fix_zotero_dates.py script to apply triggers and fix
existing bad dates. Safe to run repeatedly.
This commit is contained in:
kert
2026-03-22 01:14:58 -04:00
parent 11cc281e50
commit bf7049962f

View File

@@ -0,0 +1,81 @@
"""Fix and prevent non-ISO 8601 dates in Zotero's SQLite database.
Zotero internally writes timestamps as 'YYYY-MM-DD HH:MM:SS' (SQL format)
but its sync engine requires 'YYYY-MM-DDTHH:MM:SSZ' (ISO 8601).
This script:
1. Converts all existing space-separated timestamps to ISO 8601
2. Installs SQLite triggers that auto-convert on every INSERT/UPDATE
Safe to run multiple times (triggers use CREATE IF NOT EXISTS).
Usage:
uv run python dev/scripts/fix_zotero_dates.py
"""
from __future__ import annotations
import sqlite3
import sys
DB_PATH = "zotero/data/zotero.sqlite"
COLUMNS = ("dateAdded", "dateModified", "clientDateModified")
def main() -> int:
db = sqlite3.connect(DB_PATH, timeout=10)
db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
# Fix existing bad dates
for col in COLUMNS:
fixed = db.execute(
f"UPDATE items SET {col} = REPLACE({col}, ' ', 'T') || 'Z'"
f" WHERE {col} LIKE '____-__-__ __:__:__'"
f" AND {col} NOT LIKE '%T%'",
).rowcount
if fixed:
print(f"Fixed {col}: {fixed}")
# Install auto-fix triggers
for col in COLUMNS:
for op in ("INSERT", "UPDATE"):
trigger = f"fix_{col}_{op.lower()}"
of_clause = f"OF {col} " if op == "UPDATE" else ""
db.execute(f"""
CREATE TRIGGER IF NOT EXISTS {trigger}
AFTER {op} ON items
FOR EACH ROW
WHEN NEW.{col} LIKE '____-__-__ __:__:__'
AND NEW.{col} NOT LIKE '%T%'
BEGIN
UPDATE items SET {col} = REPLACE(NEW.{col}, ' ', 'T') || 'Z'
WHERE itemID = NEW.itemID;
END
""")
db.commit()
# Verify
triggers = [
r[0]
for r in db.execute(
"SELECT name FROM sqlite_master WHERE type='trigger' AND name LIKE 'fix_%'"
)
]
print(f"Triggers installed: {len(triggers)}")
for col in COLUMNS:
bad = db.execute(
f"SELECT COUNT(*) FROM items WHERE {col} NOT LIKE '%T%'"
).fetchone()[0]
if bad:
print(f"WARNING: {col} still has {bad} bad values")
db.close()
return 1
db.close()
print("All dates valid.")
return 0
if __name__ == "__main__":
sys.exit(main())