Warum ich das überhaupt angefangen habe
Ich halte meine Workstations gern schlank. Firefox ist mein tägliches Cockpit, aber er soll nicht ein Jahrzehnt Historie im aktiven Gedächtnis mit sich herumtragen. Trotzdem sind diese Daten – wo ich war, wann, und wie oft – Gold wert. Nicht bloß Nostalgie, sondern Signal:
- Welche Themen ich wieder und wieder aufgreife.
- Welche Domains meine Aufmerksamkeit halten.
- Wie sich Recherche über Tage oder Monate entfaltet.
- Wie „Deep Work“ im Browser eigentlich aussieht.
Also baute ich ein kleines Tool in meinem eigenen PHP-Framework Charm, das unter der Haube Laravel’s Eloquent nutzt. Auftrag: Firefox’ places.sqlite (Bookmarks, Places, Visits, Origins) in meine Datenbank laden, damit ich später archivieren, analysieren und durchsuchen kann – und Firefox danach aufräumen kann, damit dieser schnell bleibt.
Als ich begonnen habe, war meine places.sqlite
auf dem frischen Windows-10-Rechner klein. Der simpelste Importer reichte vollkommen: SQLite öffnen, Zeilen iterieren, in Eloquent firstOrNew()
, ein paar Properties setzen, save()
. Ihr kennt das Muster. Der pragmatische „Wochenend-Bonus“-Code, den man vergisst, weil er eben läuft.
Die Zeit vergeht. Die Datenbank wächst. Mein Workflow konzentriert sich auf eine Maschine. Plötzlich enthält places.sqlite
praktisch alles – und der einst harmlose Importer wird zum Anker.
Eines Tages startete ich ihn. Vier Stunden später brach ich ab. Nicht weil er nicht irgendwann fertig geworden wäre – sondern weil … das inzwischen einfach inakzeptabel ist.
Der Moment der Entscheidung
Ich reservierte mir an einem ruhigen Wochenende 1–2 entspannte Stunden. Ich kenne SQL. Ich kenne Transaktionen. Ich weiß, warum row-by-row-ORMs ab einer gewissen Größenordnung die Klippe hinunterstürzen. Das Problem war nicht Wissen – es waren Prioritäten. Früher war das Spielzeug. Jetzt ist es ein interessanter Datensatz.
Also tat ich, was ich bei Mikro-Refactors immer tue: einen Schritt zurück – und das mentale Modell neu verdrahtet.
Worum geht’s wirklich?
- Zwei Tabellen ändern sich in-place (
moz_bookmarks
,moz_origins
). - Zwei Tabellen sind im Kern append-only (
moz_places
,moz_historyvisits
). - Firefox-Zeitstempel sind Mikrosekunden seit Epoch; meine DB erwartet
timestamp
/datetime
. - Mein Ziel-Schema hat einen Prefix; die App nutzt Eloquent in Charm; der Code soll sauber bleiben.
- Und vor allem: Die Datenbank will set-basierte Arbeit – keinen Hagel aus Zeilen-Geflüster.
Ich musste nichts „Neues entdecken“. Ich musste nur die richtigen Wahrheiten anwenden – jetzt, wo die Daten es verlangen.
Den Import neu denken
Die Zeilenschleife flog raus, dafür kam ein Bulk-Loader: Chunks aus der Quell-SQLite lesen, Batches nach MariaDB schreiben, eine Transaktion pro Tabelle. Für append-only-Tabellen hole ich nur die Deltas (WHERE id > max(source_id)
). Für veränderliche Tabellen gibt’s einen Upsert über alles. Und weil Firefox-Zeit in Mikrosekunden tickt, konvertiere ich beim Laden sauber auf UTC-Timestamps.
Das war es schon. Nicht glamourös. Einfach korrekt.
Ein paar kompakte Snippets erzählen die Geschichte.
Datenbank-Setup
Ich ersetzte PHPs pures SQLite-Handling und nutze Eloquent für beide Verbindungen – inklusive Upsert-Komfort:
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Connection;
$capsule = new Capsule();
// Target: MariaDB/MySQL (your app DB)
$capsule->addConnection([
'driver' => 'mysql',
'host' => '127.0.0.1',
'database' => 'your_db',
'username' => 'user',
'password' => 'pass',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => 'ab_', // if you use a table prefix
], 'target');
// Source: Firefox places.sqlite
$capsule->addConnection([
'driver' => 'sqlite',
'database' => __DIR__.'/places.sqlite',
'prefix' => '',
], 'places');
$capsule->setAsGlobal();
$capsule->bootEloquent();
$dst = $capsule->getConnection('target');
$src = $capsule->getConnection('places');
$dst->disableQueryLog();
$src->disableQueryLog();
Der kleine Zeit-Konverter
function mozUsToTimestamp(?int $us): ?string
{
if (!$us || $us setTimezone(new DateTimeZone('UTC'));
return $dt->format('Y-m-d H:i:s'); // switch to datetime(6) and add .%06d if you need microseconds
}
Upsert für eine veränderliche Tabelle (z. B. moz_origins
)
In Charm nutze ich direkt den Query Builder von Eloquent (keine per-Row-Model-Events):
$src->table('moz_origins')
->select(['id','prefix','host','frecency'])
->orderBy('id')
->chunk(5000, function ($chunk) use ($dst) {
$now = date('Y-m-d H:i:s');
$payload = [];
foreach ($chunk as $r) {
$payload[] = [
'source_id' => $r->id,
'prefix' => $r->prefix,
'host' => $r->host,
'frecency' => $r->frecency,
'created_at' => $now,
'updated_at' => $now,
];
}
if ($payload) {
// UNIQUE(source_id) on target
$dst->table('browser_origins')->upsert(
$payload,
['source_id'],
['prefix','host','frecency','updated_at']
);
}
});
Eine Anweisung pro Chunk. MariaDB tut, wofür sie gebaut ist.
Delta-Insert für eine Append-Only-Tabelle (z. B. moz_places
)
$maxId = (int) ($dst->table('browser_places')->max('source_id') ?? 0);
$src->table('moz_places')
->select(['id','url','title','rev_host','visit_count','hidden','typed','frecency','last_visit_date','guid'])
->where('id', '>', $maxId)
->orderBy('id')
->chunk(5000, function ($chunk) use ($dst) {
$now = date('Y-m-d H:i:s');
$rows = [];
foreach ($chunk as $r) {
$rows[] = [
'source_id' => $r->id,
'url' => $r->url,
'title' => $r->title,
'rev_host' => $r->rev_host,
'visit_count' => $r->visit_count,
'hidden' => $r->hidden,
'typed' => $r->typed,
'frecency' => $r->frecency,
'last_visit_date' => mozUsToTimestamp((int)$r->last_visit_date),
'guid' => $r->guid,
'created_at' => $now,
'updated_at' => $now,
];
}
if ($rows) {
// UNIQUE(source_id) on target; duplicates are ignored by the engine
$dst->table('browser_places')->insertOrIgnore($rows);
}
});
Und ja, eine Transaktion pro Tabelle um diese Blöcke herum. Allein das schrumpft Stunden voller fsyncs zu einem kurzen, robusten Schreibvorgang.
Das Ergebnis (und warum es kein „Zauber“ ist)
Ich habe den Importer auf derselben Maschine erneut laufen lassen:
- Altes Skript: nach vier Stunden abgebrochen.
- Neuer Loader: **
Warum die places.sqlite
so interessant ist
Sobald die Daten in meinem Schema liegen, wird der Browser von einer flüchtigen UI zu einem messbaren Wissensfluss.
Ich kann einiges sehen:
- Temporale Aufmerksamkeit: nicht nur was ich lese, sondern wann – und wie es strukturiert ist. Morgens Geopolitik; nächtliche Deep-Dives in einen Recherchefaden; ein Schub Doku-Tabs während eines Refactors.
- Domain-Gravitäten: welche Sites mein Denken verankern, welche Brücken schlagen, welche bloß Rauschen sind.
- Projekt-Signaturen: wie eine Produktuntersuchung im Browser aussieht, lange bevor eine Zeile Code entsteht.
- Search-to-Absorption-Zyklen: Ein Query, ein Klick, drei Tabs später – wurde daraus eine Lern-„Schleife“ oder endete es als Sackgasse?
Es gibt hier ethische Nuancen: Das ist mein eigenes Browsing, privat archiviert auf eigener Hardware, mit dem expliziten Ziel, meine Tools besser und meinen Kopf leichter zu machen. Firefox bleibt schlank, weil ich Historie aggressiv purgen kann; mein Loader bewahrt die Spur für zukünftige Analysen.
Und auch wenn ich die Daten noch nicht tiefgreifend auswerte, erfasse ich sie strukturiert. Wenn ich Zeit für ein solches Projekt habe, arbeite ich mit ordentlich strukturierten Daten und kann mich auf das Analysesystem über einem großen Datensatz konzentrieren. Es besteht zudem kein Risiko, dass alte Historie verschwindet oder mühsam aus mehreren places.sqlite
rekonstruiert werden muss.
Zur Rolle von KI (und was sie nicht war)
Ich habe nicht „die KI gebeten, es schnell zu machen“. Das ist nicht mein Stil. Ich nutzte ChatGPT als Partner, um die Artikulation zu beschleunigen:
- Die Architektur stand – Transaktionen, set-basierte Writes, Upsert vs. Append-Only.
- Ich wollte Entscheidungen gegentesten, mir Eloquent-Details ins Gedächtnis rufen und eine saubere Oberfläche in Charm bauen, ohne in Laravel-App-Bootstrapping abzubiegen.
- Die Zusammenarbeit nahm Reibung raus: im Flow bleiben, ein paar taktische Kanten validieren (z. B. wie ich die temporäre SQLite-Connection bevorzuge) und den Fokus scharf halten.
Ein paar praktische Dinge, die mir wichtig sind
Ich erspare euch eine Checkliste, aber drei Details zählen:
- Schema-Ehrlichkeit. Liefert die Quelle Mikrosekunden und mir ist feine Kausalität wichtig, wechsle ich auf
datetime(6)
und speichere sie. Reichen Sekunden, wandle ich bewusst herunter. In jedem Fall ist es eine Entscheidung – sichtbar im Code, nicht Zufall. - Vorhersehbare Re-Runs. Ein Loader, der nur einmal funktioniert, ist eine Falle. Idempotenz, Deltas und eindeutige Keys halten das System performant, auch wenn ich zweimal auf „Start“ drücke.
- Isolation im Stack. Charm liefert Ergonomie; Eloquent die Ausdrücke. Der Importer läuft elegant. Wenn ich fertig bin, kann Firefox aufräumen – und meine Statistiken bleiben.
Wie es weitergeht
Jetzt, wo die Pipeline in wenigen Sekunden läuft, beginnt der spannende Teil:
- Explorative Statistiken, die die Realität treffen: Visit-Sequenzen, Domain-Gravitäten, Tageszeit-Muster für Deep Work, „Themen-Migration“ über Wochen.
- Eine Suche, die wirklich mir gehört: Places, Visits und Bookmarks mit meinen Notizen, Commits und Texten kreuzverbinden.
- Signale für Fokus-Management: Wenn eine Woche zu reaktivem Browsen kippt, will ich das so deutlich sehen wie fehlgeschlagene Tests.
Weil die Daten unter meiner Kontrolle sind – und der Loader schnell genug ist, um ihn gedankenlos erneut zu starten – kann ich an Fragen iterieren, ohne die Pipeline zu babysitten.
Schluss: Die leise Zufriedenheit, es richtig zu machen
Das war kein heroischer Rewrite. Es war die sanfte, unvermeidliche Korrektur eines Musters, das seinen Zweck erfüllt hatte – und dann zu lange blieb. Die erste Version war okay, als die Welt klein war. Die neue Version respektiert die Form der Daten und die Stärken der Datenbank.
Mir gefällt, dass es einen Nachmittag brauchte. Mir gefällt, dass es in Sekunden läuft. Mir gefällt, dass Firefox flink bleibt, während mein Archiv klüger wird. Und mir gefällt, dass die Lösung im besten Sinne langweilig ist: set-basiertes SQL, klare Grenzen, Werkzeuge, die nicht stören.
Wenn deine Side-Projects heimlich zu echten Daten geworden sind, lohnt die Frage: Erzählt der Code noch die Wahrheit über den Maßstab? Meiner tat es nicht. Jetzt schon.
Dieser Beitrag wurde von mir selbst verfasst – mit Unterstützung durch KI (GPT-5). Die Illustrationen stammen von mir (erstellt mit Sora). Entdecke, wie KI deine Inhalte inspirieren kann – Neoground GmbH.
Noch keine Kommentare
Kommentar hinzufügen