Der widerspenstigen sqlite-Zähmung: Ein Wochenend-Refactoring
share forum

Der widerspenstigen sqlite-Zähmung: Ein Wochenend-Refactoring


Software • by Sven Reifschneider • 24 September 2025 • 0 comments
info
This post is also available in English. Read in English

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.


Share this post

If you enjoyed this article, why not share it with your friends and acquaintances? It helps me reach more people and motivates me to keep creating awesome content for you. Just use the sharing buttons below to share the post on your favorite social media platforms. Thank you!

Please consider sharing this post
Please consider donating

Support the Blog

If you appreciate my work and this blog, I would be thrilled if you'd like to support me! For example, you can buy me a coffee to keep me refreshed while working on new articles, or simply contribute to the ongoing success of the blog. Every little bit of support is greatly appreciated!

currency_bitcoin Donate via Crypto
Bitcoin (BTC):1JZ4inmKVbM2aP5ujyvmYpzmJRCC6xS6Fu
Ethereum (ETH):0xC66B1D5ff486E7EbeEB698397F2a7b120e17A6bE
Litecoin (LTC):Laj2CkWBD1jt4ZP6g9ZQJu1GSnwEtsSGLf
About the author

Sven Reifschneider

Greetings! I'm Sven, a tech innovator and enthusiastic photographer from scenic Wetterau, near the vibrant Frankfurt/Rhein-Main area. This blog is where I fuse my extensive tech knowledge with artistic passion to craft stories that captivate and enlighten. Leading Neoground, I push the boundaries of AI consulting and digital innovation, advocating for change that resonates through community-driven technology.

Photography is my portal to expressing the ephemeral beauty of life, blending it seamlessly with technological insights. Here, art meets innovation, each post striving for excellence and sparking conversations that inspire.

Curious to learn more? Follow me on social media or click on "learn more" to explore the essence of my vision.


No comments yet

Add a comment

You can use **Markdown** in your comment. Your email won't be published. Find out more about our data protection in the privacy policy.