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