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:
81
dev/scripts/fix_zotero_dates.py
Normal file
81
dev/scripts/fix_zotero_dates.py
Normal 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())
|
||||
Reference in New Issue
Block a user