<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sqlite</title>
<link>https://coderlegion.com/tag/sqlite</link>
<description>Powered by Question2Answer</description>
<item>
<title>Offline-First Bible Reader on iOS: Bundled SQLite, Full-Text Search and iCloud-Synced Notes</title>
<link>https://coderlegion.com/32388/offline-first-bible-reader-on-ios-bundled-sqlite-full-text-search-and-icloud-synced-notes</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/building-an-offline-first-bible-reader-on-ios/&quot;&gt;https://bestlatestapps.com/blog/building-an-offline-first-bible-reader-on-ios/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;How I'd structure a scripture app that works on a plane: a read-only text database, fast search, user data that follows you between devices, read-aloud and a verse-of-the-day widget&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-an-offline-first-bible-reader-on-ios/bible-study-ai-icon-512.jpg&quot; alt=&quot;Bible Study AI app icon&quot;&gt;&lt;/p&gt;
&lt;p&gt;The first app I ever published under my App Store developer name was a Bible reader. It went live on 20 April 2020, a day ahead of the next few apps I shipped that month, and today it's called &lt;strong&gt;Bible Study AI – Read Offline&lt;/strong&gt;. It has grown a lot since then: reading plans, devotionals, a prayer journal, widgets and an AI study assistant. But the promise in the name has never changed. &lt;em&gt;It works offline.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&quot;Offline&quot; sounds like a feature you tick off. It isn't. It's an architectural decision, and it shapes everything: where the text lives, how search works, where highlights and notes are saved, how sync behaves when the network comes back, and what you do on a device that has never been online since the app was installed.&lt;/p&gt;
&lt;p&gt;This article walks through the architecture I'd recommend for this kind of app: a large, read-only text corpus plus a small amount of precious user data. Scripture is the example, but the same shape fits dictionaries, manuals, legal codes, recipe books and course material. All the code is Swift, and I've checked API names and availability against Apple's developer documentation. Where I'm describing &lt;em&gt;the approach I'd use&lt;/em&gt; rather than the app's exact internals, I say so.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-problem-two-kinds-of-data-with-opposite-needs&quot;&gt;1. The problem: two kinds of data with opposite needs&lt;/h3&gt;
&lt;p&gt;A Bible app holds two very different kinds of data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The text itself.&lt;/strong&gt; All 66 books, in several translations; the app's listing mentions KJV, ASV, Webster and World English. That's tens of thousands of verses per translation. It's large, it never changes on the device, and it must be searchable in milliseconds. It belongs to &lt;em&gt;you&lt;/em&gt;, the developer, not the user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the user creates.&lt;/strong&gt; Highlights in different colours, notes on verses and chapters, bookmarks, reading-plan progress, prayer journal entries. It's small, it changes all the time, and it's the only data the user would be upset to lose. It belongs to &lt;em&gt;them&lt;/em&gt;, and they expect it on their iPad as well as their iPhone.&lt;/p&gt;
&lt;p&gt;The mistake I see most often is putting both in one store. Then every app update that fixes a typo in the text has to migrate a database full of user notes, and every sync pushes megabytes of scripture to iCloud for no reason.&lt;/p&gt;
&lt;p&gt;So split them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;App bundle (read-only)                 User store (read-write, synced)
┌──────────────────────────┐           ┌──────────────────────────────┐
│ bible.sqlite             │           │ Core Data + CloudKit         │
│  translations, books,    │  verse    │  Highlight, Note, Bookmark,  │
│  chapters, verses        │◀─ id ────▶│  PlanProgress, Prayer        │
│  verses_fts (FTS5)       │           │                              │
└──────────────────────────┘           └──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only thing that links them is a stable &lt;strong&gt;verse identifier&lt;/strong&gt;. Get that right first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-a-verse-id-that-will-never-change&quot;&gt;2. A verse ID that will never change&lt;/h3&gt;
&lt;p&gt;A verse needs an identifier that's identical across translations, app versions and devices. I use a simple integer: &lt;code&gt;book * 1_000_000 + chapter * 1_000 + verse&lt;/code&gt;. John 3:16 is book 43, so its ID is &lt;code&gt;43_003_016&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;struct VerseRef: Hashable, Codable, Comparable {
    let book: Int      // 1...66, canonical order
    let chapter: Int
    let verse: Int

    var id: Int { book * 1_000_000 + chapter * 1_000 + verse }

    init(book: Int, chapter: Int, verse: Int) {
        self.book = book; self.chapter = chapter; self.verse = verse
    }

    init(id: Int) {
        book = id / 1_000_000
        chapter = (id / 1_000) % 1_000
        verse = id % 1_000
    }

    static func &amp;lt; (lhs: VerseRef, rhs: VerseRef) -&amp;gt; Bool { lhs.id &amp;lt; rhs.id }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why an integer and not a string like &lt;code&gt;&quot;JHN.3.16&quot;&lt;/code&gt;? It sorts correctly, ranges are cheap (&lt;code&gt;BETWEEN 43003001 AND 43003999&lt;/code&gt; is &quot;all of John 3&quot;), and it's a compact key in both SQLite and Core Data. A highlight saved against &lt;code&gt;43003016&lt;/code&gt; shows up in KJV and in World English alike, because the ID describes the &lt;em&gt;position&lt;/em&gt;, not the wording.&lt;/p&gt;
&lt;p&gt;One caveat from experience: versification isn't identical in every tradition. A few translations number some verses differently. If you add a translation that disagrees, map it onto your canonical scheme when you &lt;em&gt;build&lt;/em&gt; the database, never at runtime.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-shipping-the-text-as-a-bundled-read-only-sqlite-database&quot;&gt;3. Shipping the text as a bundled, read-only SQLite database&lt;/h3&gt;
&lt;p&gt;I build the text database offline with a script, add &lt;code&gt;bible.sqlite&lt;/code&gt; to the app target and open it &lt;strong&gt;read-only straight from the bundle&lt;/strong&gt;. No copying to Documents, no migrations, no risk of corruption.&lt;/p&gt;
&lt;p&gt;The schema is tiny:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;CREATE TABLE verses (
  translation TEXT NOT NULL,   -- 'KJV', 'ASV', 'WEB', ...
  id          INTEGER NOT NULL, -- VerseRef.id
  text        TEXT NOT NULL,
  PRIMARY KEY (translation, id)
) WITHOUT ROWID;

-- Full-text index (built by the script, shipped in the file)
CREATE VIRTUAL TABLE verses_fts USING fts5(
  text, translation UNINDEXED, id UNINDEXED,
  tokenize = 'unicode61 remove_diacritics 2'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Opening it from Swift with the system SQLite library:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import Foundation
import SQLite3

enum TextStoreError: Error { case openFailed(String), queryFailed(String) }

final class BibleTextStore {
    private var db: OpaquePointer?

    init() throws {
        guard let url = Bundle.main.url(forResource: &quot;bible&quot;, withExtension: &quot;sqlite&quot;) else {
            throw TextStoreError.openFailed(&quot;bible.sqlite missing from bundle&quot;)
        }
        // immutable=1 tells SQLite the file can never change, so it skips locking.
        let uri = &quot;file:\(url.path)?immutable=1&quot;
        let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_URI
        guard sqlite3_open_v2(uri, &amp;amp;db, flags, nil) == SQLITE_OK else {
            throw TextStoreError.openFailed(String(cString: sqlite3_errmsg(db)))
        }
    }

    deinit { sqlite3_close(db) }

    func chapter(_ book: Int, _ chapter: Int, translation: String) throws -&amp;gt; [(VerseRef, String)] {
        let sql = &quot;SELECT id, text FROM verses WHERE translation = ? AND id BETWEEN ? AND ? ORDER BY id&quot;
        var stmt: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil) == SQLITE_OK else {
            throw TextStoreError.queryFailed(String(cString: sqlite3_errmsg(db)))
        }
        defer { sqlite3_finalize(stmt) }

        let start = VerseRef(book: book, chapter: chapter, verse: 0).id
        sqlite3_bind_text(stmt, 1, translation, -1, SQLITE_TRANSIENT)
        sqlite3_bind_int64(stmt, 2, Int64(start))
        sqlite3_bind_int64(stmt, 3, Int64(start + 999))

        var rows: [(VerseRef, String)] = []
        while sqlite3_step(stmt) == SQLITE_ROW {
            let id = Int(sqlite3_column_int64(stmt, 0))
            let text = String(cString: sqlite3_column_text(stmt, 1))
            rows.append((VerseRef(id: id), text))
        }
        return rows
    }
}

// The C macro SQLITE_TRANSIENT doesn't import into Swift; this is the usual stand-in.
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why raw SQLite and not Core Data for the text? Core Data is great for object graphs you edit. A read-only corpus with a full-text index is a different job, and SQLite's FTS5 does that job directly. You can wrap this in GRDB or another library if you prefer; the shape doesn't change.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-search-that-feels-instant&quot;&gt;4. Search that feels instant&lt;/h3&gt;
&lt;p&gt;The listing promises &quot;Search across all books, chapters, and verses&quot; and &quot;Advanced search with history&quot;. With an FTS5 index shipped in the file, the query is one statement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;extension BibleTextStore {
    struct Hit { let ref: VerseRef; let snippet: String }

    func search(_ query: String, translation: String, limit: Int = 100) throws -&amp;gt; [Hit] {
        // Turn &quot;love one another&quot; into &quot;love* one* another*&quot;: prefix match on every word.
        let terms = query
            .split(whereSeparator: { !$0.isLetter &amp;amp;&amp;amp; !$0.isNumber })
            .map { &quot;\($0)*&quot; }
            .joined(separator: &quot; &quot;)
        guard !terms.isEmpty else { return [] }

        let sql = &quot;&quot;&quot;
        SELECT id, snippet(verses_fts, 0, '[', ']', '…', 12)
        FROM verses_fts
        WHERE verses_fts MATCH ? AND translation = ?
        ORDER BY rank LIMIT ?
        &quot;&quot;&quot;
        var stmt: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil) == SQLITE_OK else {
            throw TextStoreError.queryFailed(String(cString: sqlite3_errmsg(db)))
        }
        defer { sqlite3_finalize(stmt) }
        sqlite3_bind_text(stmt, 1, terms, -1, SQLITE_TRANSIENT)
        sqlite3_bind_text(stmt, 2, translation, -1, SQLITE_TRANSIENT)
        sqlite3_bind_int(stmt, 3, Int32(limit))

        var hits: [Hit] = []
        while sqlite3_step(stmt) == SQLITE_ROW {
            hits.append(Hit(ref: VerseRef(id: Int(sqlite3_column_int64(stmt, 0))),
                            snippet: String(cString: sqlite3_column_text(stmt, 1))))
        }
        return hits
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things that matter more than the SQL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sanitise the query.&lt;/strong&gt; FTS5 has its own query syntax; a stray quote or hyphen from the user can cause a syntax error. Splitting on non-alphanumerics and rebuilding the string, as above, avoids that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debounce in the UI.&lt;/strong&gt; Search on every keystroke, but cancel the previous task. A &lt;code&gt;.task(id: query)&lt;/code&gt; modifier in SwiftUI with a short &lt;code&gt;Task.sleep&lt;/code&gt; at the top gives you that for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run it off the main thread.&lt;/strong&gt; It's fast, but &quot;fast&quot; on an older iPhone in Low Power Mode isn't the same as on your desk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search history is user data&lt;/strong&gt;, so it goes in the user store (next section), not the read-only file.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-user-data-core-data-with-cloudkit-mirroring&quot;&gt;5. User data: Core Data with CloudKit mirroring&lt;/h3&gt;
&lt;p&gt;The listing says &quot;iCloud sync across all your devices… Access your highlights, notes, and bookmarks anywhere&quot;. The app supports iOS 16.0 and later, which rules out SwiftData (iOS 17+). The natural fit is &lt;strong&gt;&lt;code&gt;NSPersistentCloudKitContainer&lt;/code&gt;&lt;/strong&gt; (iOS 13+), which keeps a normal local Core Data store and mirrors it to the user's private CloudKit database in the background.&lt;/p&gt;
&lt;p&gt;That's exactly the offline-first behaviour you want: every write goes to the local store first and works with no network; sync catches up later.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import CoreData

final class UserStore {
    static let shared = UserStore()
    let container: NSPersistentCloudKitContainer

    private init() {
        container = NSPersistentCloudKitContainer(name: &quot;UserData&quot;) // UserData.xcdatamodeld

        guard let description = container.persistentStoreDescriptions.first else {
            fatalError(&quot;No store description&quot;)
        }
        description.cloudKitContainerOptions =
            NSPersistentCloudKitContainerOptions(containerIdentifier: &quot;iCloud.com.example.bible&quot;)
        // Needed so the app can react to changes that arrive from other devices.
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber,
                              forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores { _, error in
            if let error { print(&quot;Store failed to load: \(error)&quot;) }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Highlight&lt;/code&gt; entity is deliberately boring: &lt;code&gt;verseID&lt;/code&gt; (Integer 64), &lt;code&gt;translation&lt;/code&gt; (String, optional), &lt;code&gt;colorName&lt;/code&gt; (String), &lt;code&gt;createdAt&lt;/code&gt; (Date) and a UUID &lt;code&gt;id&lt;/code&gt;. A few CloudKit rules shape that model, and they catch everyone once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every attribute must be optional or have a default value.&lt;/strong&gt; CloudKit can deliver records in any order and partially.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No unique constraints.&lt;/strong&gt; CloudKit mirroring doesn't support them. If the user highlights the same verse on two offline devices, you'll get two rows; de-duplicate in code (keep the newest per &lt;code&gt;verseID&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relationships must be optional&lt;/strong&gt; and need inverses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable the capabilities:&lt;/strong&gt; iCloud with CloudKit and your container, plus Background Modes → Remote notifications, so pushes from CloudKit can wake the app to sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy the schema to production&lt;/strong&gt; in the CloudKit Console before you ship. A development schema works on your device and silently fails for users. During development, &lt;code&gt;initializeCloudKitSchema(options:)&lt;/code&gt; can create the schema from your model for you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Saving a highlight is then plain Core Data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;extension UserStore {
    func toggleHighlight(_ ref: VerseRef, color: String) throws {
        let context = container.viewContext
        let request = NSFetchRequest&amp;lt;NSManagedObject&amp;gt;(entityName: &quot;Highlight&quot;)
        request.predicate = NSPredicate(format: &quot;verseID == %lld&quot;, Int64(ref.id))

        let existing = try context.fetch(request)
        if existing.isEmpty {
            let h = NSEntityDescription.insertNewObject(forEntityName: &quot;Highlight&quot;, into: context)
            h.setValue(UUID(), forKey: &quot;id&quot;)
            h.setValue(Int64(ref.id), forKey: &quot;verseID&quot;)
            h.setValue(color, forKey: &quot;colorName&quot;)
            h.setValue(Date(), forKey: &quot;createdAt&quot;)
        } else {
            existing.forEach(context.delete) // also cleans up offline duplicates
        }
        try context.save()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the reading screen, fetch highlights for the chapter's ID range and merge them with the verse rows from SQLite in a view model. The two stores never need to know about each other.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-listen-mode-with-avspeechsynthesizer&quot;&gt;6. Listen mode with AVSpeechSynthesizer&lt;/h3&gt;
&lt;p&gt;The listing's &quot;Listen &amp;amp; Read&quot; section mentions text-to-speech and auto-scroll. On iOS that's &lt;code&gt;AVSpeechSynthesizer&lt;/code&gt; (iOS 7+), and its delegate tells you which range of text is about to be spoken, which is all you need to highlight and scroll along.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import AVFoundation

@MainActor
final class ReadAloud: NSObject, ObservableObject, AVSpeechSynthesizerDelegate {
    @Published var currentVerse: VerseRef?
    private let synth = AVSpeechSynthesizer()
    private var refs: [ObjectIdentifier: VerseRef] = [:]

    override init() {
        super.init()
        synth.delegate = self
    }

    func speak(chapter verses: [(VerseRef, String)], language: String = &quot;en-US&quot;) {
        synth.stopSpeaking(at: .immediate)
        refs = [:]
        for (ref, text) in verses {
            let u = AVSpeechUtterance(string: text)
            u.voice = AVSpeechSynthesisVoice(language: language)
            u.rate = AVSpeechUtteranceDefaultSpeechRate
            u.postUtteranceDelay = 0.15        // a breath between verses
            refs[ObjectIdentifier(u)] = ref
            synth.speak(u)                     // utterances are queued in order
        }
    }

    func pause() { synth.pauseSpeaking(at: .word) }
    func resume() { synth.continueSpeaking() }

    nonisolated func speechSynthesizer(_ s: AVSpeechSynthesizer,
                                       didStart utterance: AVSpeechUtterance) {
        let key = ObjectIdentifier(utterance)
        Task { @MainActor in
            self.currentVerse = self.refs[key]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The view observes &lt;code&gt;currentVerse&lt;/code&gt; and calls &lt;code&gt;ScrollViewReader.scrollTo(_:anchor:)&lt;/code&gt; with the verse ID. For word-level highlighting, implement &lt;code&gt;speechSynthesizer(_:willSpeakRangeOfSpeechString:utterance:)&lt;/code&gt; as well.&lt;/p&gt;
&lt;p&gt;Two practical notes. If you want speech to continue with the screen locked (&quot;perfect for listening while commuting&quot;), set the audio session category to &lt;code&gt;.playback&lt;/code&gt; and add the &lt;code&gt;audio&lt;/code&gt; background mode, and only while speech is actually running. And the voices available vary by device and by what the user has downloaded in Settings, so let them pick, and fall back gracefully when &lt;code&gt;AVSpeechSynthesisVoice(language:)&lt;/code&gt; returns &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-verse-of-the-day-on-the-home-screen-with-widgetkit&quot;&gt;7. Verse of the Day on the Home Screen with WidgetKit&lt;/h3&gt;
&lt;p&gt;The listing has &quot;Home screen widgets: quick access to verse of the day&quot;. A widget runs in its own extension process, so it can't reach into the app's memory. Two things make this simple with the architecture above:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The text database is read-only and in the bundle&lt;/strong&gt;, so the widget extension can ship its own copy (or you can share it through an App Group container). No sync needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The verse of the day is a deterministic function of the date.&lt;/strong&gt; Every device shows the same verse without a server.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import WidgetKit
import SwiftUI

struct VerseEntry: TimelineEntry {
    let date: Date
    let reference: String
    let text: String
}

struct VerseProvider: TimelineProvider {
    func placeholder(in context: Context) -&amp;gt; VerseEntry {
        VerseEntry(date: .now, reference: &quot;John 3:16&quot;, text: &quot;For God so loved the world…&quot;)
    }

    func getSnapshot(in context: Context, completion: @escaping (VerseEntry) -&amp;gt; Void) {
        completion(entry(for: .now))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline&amp;lt;VerseEntry&amp;gt;) -&amp;gt; Void) {
        let now = Date()
        let tomorrow = Calendar.current.startOfDay(for: now.addingTimeInterval(86_400))
        completion(Timeline(entries: [entry(for: now)], policy: .after(tomorrow)))
    }

    private func entry(for date: Date) -&amp;gt; VerseEntry {
        // Pick from a curated list by day-of-year: same verse for everyone, every device.
        let day = Calendar.current.ordinality(of: .day, in: .year, for: date) ?? 1
        let pick = DailyVerses.list[(day - 1) % DailyVerses.list.count]
        return VerseEntry(date: date, reference: pick.reference, text: pick.text)
    }
}

struct VerseOfTheDayWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: &quot;VerseOfTheDay&quot;, provider: VerseProvider()) { entry in
            VStack(alignment: .leading, spacing: 6) {
                Text(entry.text).font(.callout).lineLimit(5)
                Text(entry.reference).font(.caption).foregroundStyle(.secondary)
            }
            .containerBackground(.fill.tertiary, for: .widget) // iOS 17+
        }
        .configurationDisplayName(&quot;Verse of the Day&quot;)
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DailyVerses.list&lt;/code&gt; is a small curated array compiled into the extension. With an iOS 16 deployment target, wrap &lt;code&gt;containerBackground&lt;/code&gt; in an availability check, since that modifier is iOS 17+. The &lt;code&gt;.after(tomorrow)&lt;/code&gt; policy asks WidgetKit to refresh just after midnight. It's a request, not a guarantee, so the entry should still read correctly if WidgetKit refreshes a bit late.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-real-world-example-bible-study-ai-read-offline&quot;&gt;8. Real-world example: Bible Study AI – Read Offline&lt;/h3&gt;
&lt;p&gt;All of this comes from building and maintaining my own app, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/bible-study-ai-read-offline/id1508795537&quot;&gt;Bible Study AI – Read Offline&lt;/a&gt;&lt;/strong&gt;. The patterns above are the approach I'd use and recommend; I'm describing the architecture, not giving a line-by-line tour of the shipping code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-an-offline-first-bible-reader-on-ios/bible-study-ai-screenshot-1.jpg&quot; alt=&quot;Bible Study AI App Store screenshot 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;At the time of writing, its public App Store listing shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free&lt;/strong&gt;, in the &lt;strong&gt;Books&lt;/strong&gt; and &lt;strong&gt;Education&lt;/strong&gt; categories&lt;/li&gt;
&lt;li&gt;First released in &lt;strong&gt;April 2020&lt;/strong&gt;: my first app on the store&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;113 ratings&lt;/strong&gt; in the US store, averaging about &lt;strong&gt;4.6 stars&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Supports &lt;strong&gt;iOS 16.0 and later&lt;/strong&gt;, on iPhone and iPad&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's how the listing's features map to the sections above:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What the listing says &lt;/th&gt;&lt;th&gt;Where it's covered&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;Read all 66 books of the Bible offline&quot; / &quot;Multiple translations: KJV, ASV, Webster, World English, and more&quot; &lt;/td&gt;&lt;td&gt;Bundled read-only SQLite, one table keyed by translation + verse ID (sections 2–3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Search across all books, chapters, and verses&quot; / &quot;Advanced search with history&quot; &lt;/td&gt;&lt;td&gt;FTS5 index shipped in the file, sanitised prefix queries (section 4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Highlight verses with multiple colors&quot; / &quot;Add personal notes&quot; / &quot;Bookmark favorite passages&quot; &lt;/td&gt;&lt;td&gt;Small Core Data entities keyed by verse ID (section 5)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;iCloud sync across all your devices&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;NSPersistentCloudKitContainer&lt;/code&gt; mirroring to the private database (section 5)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Text-to-speech support&quot; / &quot;Auto-scroll for hands-free reading&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;AVSpeechSynthesizer&lt;/code&gt; and its delegate driving the scroll position (section 6)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Home screen widgets: quick access to verse of the day&quot; &lt;/td&gt;&lt;td&gt;WidgetKit &lt;code&gt;StaticConfiguration&lt;/code&gt;, date-derived entry (section 7)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-an-offline-first-bible-reader-on-ios/bible-study-ai-screenshot-2.jpg&quot; alt=&quot;Bible Study AI App Store screenshot 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;The listing also covers reading plans, daily devotionals, a prayer journal with reminders and a &quot;Bible Study Assistant&quot; for AI-powered answers. Those all sit on top of the same split: plan progress and prayers are user data in the synced store, reminders are local notifications, and the assistant is the one feature that needs a network. I've left the AI part out of this article on purpose. It's the one piece that &lt;em&gt;isn't&lt;/em&gt; offline, and the most important design rule there is simply to make sure the rest of the app never waits on it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-before-you-ship-an-offline-first-reader-a-checklist&quot;&gt;9. Before you ship an offline-first reader: a checklist&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Airplane-mode test from a fresh install.&lt;/strong&gt; Delete the app, turn on Airplane Mode, install from TestFlight… then read, search, highlight, take a note and listen. Everything except network features must work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text and user data are separate.&lt;/strong&gt; The corpus is read-only in the bundle; user data lives in its own store. An app update that changes the text never touches user data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable IDs everywhere.&lt;/strong&gt; Every highlight, note and bookmark points at a canonical verse ID, never at a row number or a translation-specific string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The CloudKit model follows the rules.&lt;/strong&gt; Optional attributes or defaults, no unique constraints, optional relationships with inverses, and the schema is deployed to production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duplicates are handled.&lt;/strong&gt; Two offline devices editing the same verse produce two records; your code merges them predictably.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search can't crash on user input.&lt;/strong&gt; Quotes, hyphens, emoji and other scripts are all tested against your FTS query builder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background audio is honest.&lt;/strong&gt; The &lt;code&gt;audio&lt;/code&gt; mode is used only while read-aloud is playing, and the session is deactivated when it stops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The widget works with no app launch.&lt;/strong&gt; Install, add the widget, never open the app: it should still show a verse.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Offline-first sounds like a constraint, but for a reading app it makes things simpler. A read-only database in the bundle can't be corrupted and never needs migrating. A small, separate user store syncs cheaply. And a verse ID that never changes ties the two together. Add &lt;code&gt;AVSpeechSynthesizer&lt;/code&gt; and WidgetKit and you get most of what people want from a daily reading app, without a single required network call.&lt;/p&gt;
&lt;p&gt;If you want to see where this ended up, try the app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bible Study AI – Read Offline (iOS):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/bible-study-ai-read-offline/id1508795537&quot;&gt;https://apps.apple.com/us/app/bible-study-ai-read-offline/id1508795537&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All of my free iOS apps:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/developer/alok-singh/id1508795536&quot;&gt;https://apps.apple.com/us/developer/alok-singh/id1508795536&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/alok/&quot;&gt;https://bestlatestapps.com/alok/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you've built a large offline corpus on iOS and solved sync or search differently, tell me in the comments. I'm always curious how other people draw the line between &quot;bundled&quot; and &quot;user&quot; data.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;— Alok Kumar Singh&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/building-an-offline-first-bible-reader-on-ios/&quot;&gt;https://bestlatestapps.com/blog/building-an-offline-first-bible-reader-on-ios/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32388/offline-first-bible-reader-on-ios-bundled-sqlite-full-text-search-and-icloud-synced-notes</guid>
<pubDate>Sun, 25 Oct 2026 12:40:54 +0000</pubDate>
</item>
<item>
<title>SQLite's Two-Person Team Just Passed Its First AI Stress Test</title>
<link>https://coderlegion.com/27308/sqlites-two-person-team-just-passed-its-first-ai-stress-test</link>
<description>&lt;p&gt;This spring, open source maintainers across the internet started noticing the same strange thing at the same time: a flood of bug reports that no human had written. Not spam. Not malicious. Just confidently wrong, at a scale nobody had planned for. D. Richard Hipp, the creator of SQLite, watched it happen to his own project and lived to describe it calmly, a few months later, from his home office in Charlotte.&lt;/p&gt;
&lt;p&gt;&quot;This spring, late spring into summer, there was just an avalanche of bug reports that were generated by AIs,&quot; Hipp said. &quot;And it's not just us. Every open source project I've talked to has experienced the same thing.&quot;&lt;/p&gt;
&lt;p&gt;That's worth sitting with for a second, because SQLite isn't a side project. It's the database engine sitting inside your phone, your browser, and most of the apps on both. Hipp built it in 2000 as a Navy contractor, frustrated that he kept getting blamed for outages caused by a database administrator's server, not his code. His fix was to get rid of the server entirely. &quot;You don't want to have to install Postgres in order to run Firefox,&quot; he said. Twenty-six years later, that decision is why SQLite runs on something like a trillion devices without anyone thinking about it.&lt;/p&gt;
&lt;p&gt;So when the AI bug reports hit, they hit hard, and some of them were real. Most, Hipp said, landed on obscure, untested files that had been sitting in the source tree for years, never meant to ship. A few were serious. What surprised him wasn't the volume. It was how fast it stopped. &quot;It's as if the AIs have discovered all the bugs they're able to discover, and we're through that,&quot; he said. &quot;I'm pleasantly surprised that this went away so quickly.&quot; Compare that to the profile-guided fuzzing wave a decade ago, which took years to work through. This one burned out in a few months. Hipp isn't declaring victory for AI or against it — he compares it to security conferences like Black Hat, where the same tools that help attackers eventually help defenders more. &quot;That's been true of what we've done for the last 100 years,&quot; he said.&lt;/p&gt;
&lt;p&gt;Here's the part that should get developers' attention: Hipp isn't avoiding AI. He's using it, carefully, in exactly one place. &quot;I run all the new patches through Claude now,&quot; he said. &quot;Claude does code review — it doesn't write any code for it.&quot; It catches real logic problems and the small stuff, like a comment nobody updated. That's a meaningful admission from someone who has kept SQLite's core team at five committers — two of whom do nearly all the work — while Postgres runs on 40-plus contributors and DuckDB reportedly staffs 24 people in Amsterdam alone. Hipp says that's only possible because of SQLite's 100% branch test coverage, built by hand over roughly a year of 80-hour weeks. The rigor is what lets two people move fast without breaking things. AI review adds a second set of eyes on top of that, not instead of it.&lt;/p&gt;
&lt;p&gt;What AI still doesn't get is a seat writing SQLite's core. &quot;I've tried that, and what I've seen is that the AI-generated code is not up to snuff,&quot; Hipp said, then immediately qualified it: &quot;I've noticed even just over the past year that it's getting better. My opinion could possibly change.&quot; Right now, though, the answer is no. It's all human-written.&lt;/p&gt;
&lt;p&gt;That same restraint shows up in how Hipp treats the ecosystem building on top of him. Forks like libSQL and platforms like Turso and Cloudflare D1 add concurrent writes, replication, and edge distribution — features SQLite deliberately doesn't ship. Hipp isn't threatened by it. &quot;I'm fine with someone else filling it,&quot; he said. &quot;It's public domain. You can do that. Do what makes you happy.&quot; He's also seen the pattern before: &quot;They tend to be venture capital backed, which means we've seen a lot of them come and go. Mostly gone.&quot; He built SQLite for local, embedded state in 2000 for reasons that had nothing to do with AI agents. That it turned out to be exactly what 2026's edge and agent workloads need is a convergence he didn't predict — he's on record dismissing large language models entirely back in the 1980s, for lack of data and compute. &quot;Boy, was I wrong,&quot; he said, without much drama about it.&lt;/p&gt;
&lt;p&gt;For developers deciding when SQLite is the right call, Hipp's rule is simpler than most decision trees: keep the database and the data on the same device. If your data lives somewhere else, or you're past roughly 17 terabytes, or you need many applications hammering writes at once, reach for a client-server database instead. And if you're already using SQLite, don't just dump JSON into it and call it a day — he sees too many developers treating it like a document store when the relational model would serve them better.&lt;/p&gt;
&lt;p&gt;Hipp has pledged to keep maintaining SQLite through 2050. He's about halfway there. What he said about why matters more than the date: thinking in decades instead of toward a release, an IPO, or a VC's milestone &quot;changes the way you code.&quot; He's also honest about the one thing that could end the project entirely — not a rival database, but AI good enough to rewrite SQLite from scratch, smaller and faster, with fewer bugs. &quot;We're not there yet,&quot; he said. &quot;But that might happen. And when that happens, fine. I'm done.&quot;&lt;/p&gt;
&lt;p&gt;That's not resignation. It's the same stubbornness that built the thing in the first place, applied one more time to whatever comes next.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27308/sqlites-two-person-team-just-passed-its-first-ai-stress-test</guid>
<pubDate>Tue, 15 Sep 2026 16:47:36 +0000</pubDate>
</item>
<item>
<title>Alembic in Practice: Structure, Revisions, and the Traps SQLite Sets</title>
<link>https://coderlegion.com/26691/alembic-in-practice-structure-revisions-and-the-traps-sqlite-sets</link>
<description>&lt;p&gt;&lt;em&gt;What those hex filenames mean, why migrations silently applied to the wrong database for two months, and how to widen a CHECK constraint without quietly breaking it.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Alembic is the migration tool for SQLAlchemy. Most tutorials get you as far as&lt;br&gt;
&lt;code&gt;alembic revision --autogenerate&lt;/code&gt; and stop, which leaves you unprepared for the&lt;br&gt;
two things that actually go wrong: SQLite's refusal to alter constraints, and&lt;br&gt;
configuration that points migrations at a different database than your&lt;br&gt;
application uses.&lt;/p&gt;
&lt;p&gt;This post covers both, using real migrations from&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/boazleleina/finops-sentinel&quot;&gt;FinOps Sentinel&lt;/a&gt;, an AWS cost&lt;br&gt;
agent with four migrations, one of which took three attempts to get right.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-layout&quot;&gt;The layout&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;alembic.ini                     # config; script location, logging
alembic/
├── env.py                      # runs on every alembic command
├── script.py.mako              # template for new revision files
└── versions/
    ├── c447645246ac_initial_schema.py
    ├── 08872b80af2e_notifications_and_remediations.py
    ├── a1c9f4d27b13_add_rds_and_s3_resource_types.py
    └── b7e2f81c4a90_add_spend_snapshots.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plus one table Alembic creates in your database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One row. One value. That is the entirety of Alembic's state: which database is&lt;br&gt;
at which revision. Everything else is derived.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-the-filenames-look-like-keyboard-mashing&quot;&gt;Why the filenames look like keyboard mashing&lt;/h3&gt;
&lt;p&gt;This is the most common first question, and the answer explains the design.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;c447645246ac_initial_schema.py
b7e2f81c4a90_add_spend_snapshots.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hex prefix is the &lt;strong&gt;revision ID&lt;/strong&gt;: a 12-character slice of a UUID, generated&lt;br&gt;
by &lt;code&gt;alembic revision&lt;/code&gt;. The filename is &lt;code&gt;&amp;lt;revision_id&amp;gt;_&amp;lt;your message slugified&amp;gt;.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside each file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;revision: str = &quot;b7e2f81c4a90&quot;
down_revision: Union[str, None] = &quot;a1c9f4d27b13&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those two fields form a &lt;strong&gt;linked list&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;c447645246ac  initial_schema                  (down_revision = None)  ← root
      ↓
08872b80af2e  notifications_and_remediations
      ↓
a1c9f4d27b13  add_rds_and_s3_resource_types
      ↓
b7e2f81c4a90  add_spend_snapshots                                     ← head
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Order comes from the pointers, never from filenames.&lt;/strong&gt; Look at the project's&lt;br&gt;
list again: &lt;code&gt;b7e2f81c4a90&lt;/code&gt; sorts alphabetically &lt;em&gt;before&lt;/em&gt; &lt;code&gt;c447645246ac&lt;/code&gt;, yet&lt;br&gt;
&lt;code&gt;c447645246ac&lt;/code&gt; is the first migration and &lt;code&gt;b7e2f81c4a90&lt;/code&gt; is the newest. If&lt;br&gt;
Alembic sorted by filename, everything would run backwards.&lt;/p&gt;
&lt;h4 id=&quot;why-not-0001-0002-0003-&quot;&gt;Why not 0001, 0002, 0003?&lt;/h4&gt;
&lt;p&gt;Because sequential numbers collide the moment two people work in parallel.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;main
 ├── feature/billing   → 0004_add_invoices.py
 └── feature/auth      → 0004_add_sessions.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are valid. Both are &lt;code&gt;0004&lt;/code&gt;. Merging produces a conflict where the numbering&lt;br&gt;
lies about order, and worse, the conflict is in the &lt;em&gt;filename&lt;/em&gt;, and git resolves it&lt;br&gt;
by keeping both, and now your migration history has two fourth steps.&lt;/p&gt;
&lt;p&gt;With hex IDs there is no collision. And if both branches set &lt;code&gt;down_revision&lt;/code&gt; to&lt;br&gt;
the same parent, Alembic detects a genuine branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ alembic upgrade head
ERROR: Multiple head revisions are present
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It refuses to guess, and you resolve it explicitly with &lt;code&gt;alembic merge&lt;/code&gt;. The&lt;br&gt;
scary-looking IDs are what make that detection possible.&lt;/p&gt;
&lt;h4 id=&quot;if-you-want-readable-filenames&quot;&gt;If you want readable filenames&lt;/h4&gt;
&lt;p&gt;Add to &lt;code&gt;alembic.ini&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s_%%(slug)s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giving &lt;code&gt;20260728_b7e2f81c4a90_add_spend_snapshots.py&lt;/code&gt;: date-sortable, revision&lt;br&gt;
ID still present because Alembic needs it. Existing files keep their names; only&lt;br&gt;
new ones pick it up.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-configuration-trap-that-cost-two-months&quot;&gt;The configuration trap that cost two months&lt;/h3&gt;
&lt;p&gt;This is the bug I most want to warn people about, because it produces &lt;strong&gt;no&lt;br&gt;
error message at all&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The default &lt;code&gt;env.py&lt;/code&gt; reads the database URL from &lt;code&gt;alembic.ini&lt;/code&gt;. FinOps Sentinel&lt;br&gt;
needed it configurable, so early on it did the obvious thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# alembic/env.py: the version with the bug
def get_url():
    db_path = os.getenv(&quot;SENTINEL_DB_PATH&quot;, &quot;.sentinel.db&quot;)
    return f&quot;sqlite:///{db_path}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reasonable. Except the application reads the same setting through&lt;br&gt;
pydantic-settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Settings(BaseSettings):
    sentinel_db_path: str = &quot;.sentinel.db&quot;
    model_config = SettingsConfigDict(env_file=&quot;.env&quot;)   # ← reads .env too
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;os.getenv&lt;/code&gt; does not read &lt;code&gt;.env&lt;/code&gt;. pydantic-settings does.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The documented setup puts &lt;code&gt;SENTINEL_DB_PATH=data/sentinel.db&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, not in&lt;br&gt;
the shell. So:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;Reads from &lt;/th&gt;&lt;th&gt;Resolves to&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;The app &lt;/td&gt;&lt;td&gt;env var → &lt;code&gt;.env&lt;/code&gt; → default &lt;/td&gt;&lt;td&gt;&lt;code&gt;data/sentinel.db&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Alembic &lt;/td&gt;&lt;td&gt;env var → default &lt;/td&gt;&lt;td&gt;&lt;code&gt;.sentinel.db&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every migration ever run landed on &lt;code&gt;.sentinel.db&lt;/code&gt;. The application used&lt;br&gt;
&lt;code&gt;data/sentinel.db&lt;/code&gt;. Two files, silently, for two months.&lt;/p&gt;
&lt;h4 id=&quot;why-nothing-broke-for-so-long&quot;&gt;Why nothing broke for so long&lt;/h4&gt;
&lt;p&gt;The application also calls &lt;code&gt;Base.metadata.create_all()&lt;/code&gt; in tests, and the first&lt;br&gt;
three migrations only created tables that SQLAlchemy would have created anyway.&lt;br&gt;
The schemas &lt;em&gt;happened&lt;/em&gt; to agree.&lt;/p&gt;
&lt;p&gt;Then came a migration whose table nothing else creates: &lt;code&gt;spend_snapshots&lt;/code&gt;. The&lt;br&gt;
next scan died:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;alembic upgrade head
INFO  [alembic.runtime.migration] Running upgrade a1c9f4d27b13 -&amp;gt; b7e2f81c4a90

sentinel scan
OperationalError: (sqlite3.OperationalError) no such table: spend_snapshots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alembic reported success. The app said the table didn't exist. Both were telling&lt;br&gt;
the truth about different files.&lt;/p&gt;
&lt;h4 id=&quot;the-fix-one-owner-for-the-url&quot;&gt;The fix: one owner for the URL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# config.py: the single source of truth
def database_url() -&amp;gt; str:
    &quot;&quot;&quot;The one place the findings database URL is built.

    Both the application (bootstrap.get_repository) and Alembic (alembic/env.py)
    call this. They used to build it independently, and drifted.
    &quot;&quot;&quot;
    return f&quot;sqlite:///{settings.sentinel_db_path}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# alembic/env.py
from finops_sentinel.config import database_url

def get_url():
    return database_url()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# bootstrap.py
def get_repository() -&amp;gt; FindingsRepository:
    return SqlAlchemyRepository(db_url=database_url())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;alembic.ini&lt;/code&gt; is deliberately emptied:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# Deliberately empty. env.py overrides this with the application's own
# Settings.sentinel_db_path. A URL here would be dead config that reads as
# authoritative, which is exactly how this drifted before.
sqlalchemy.url =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Takeaway: if your app and your migrations resolve the database path&lt;br&gt;
separately, they will eventually disagree, and nothing will tell you.&lt;/strong&gt; Give the&lt;br&gt;
URL exactly one owner and have both call it.&lt;/p&gt;
&lt;p&gt;The project now pins this with tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_the_app_and_alembic_build_the_url_the_same_way():
    assert get_repository().db_url == database_url()

def test_alembic_ini_does_not_hardcode_a_database():
    parser = configparser.ConfigParser()
    parser.read(REPO_ROOT / &quot;alembic.ini&quot;)
    assert parser.get(&quot;alembic&quot;, &quot;sqlalchemy.url&quot;, fallback=&quot;&quot;).strip() == &quot;&quot;

def test_alembic_env_does_not_read_the_environment_itself():
    env_py = (REPO_ROOT / &quot;alembic&quot; / &quot;env.py&quot;).read_text()
    assert &quot;database_url&quot; in env_py
    assert &quot;os.getenv(&quot; not in env_py
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;autogenerate-and-where-to-stop-trusting-it&quot;&gt;Autogenerate, and where to stop trusting it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alembic revision --autogenerate -m &quot;add spend snapshots&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alembic compares your SQLAlchemy models against the live database and writes the&lt;br&gt;
diff. It needs &lt;code&gt;target_metadata&lt;/code&gt; wired up in &lt;code&gt;env.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from finops_sentinel.adapters.persistence.sqlalchemy_repo import Base
target_metadata = Base.metadata
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;It detects&lt;/strong&gt; new/dropped tables and columns, nullability changes, most index&lt;br&gt;
and unique-constraint changes, and some type changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It misses&lt;/strong&gt; table and column &lt;em&gt;renames&lt;/em&gt; (it sees a drop plus an add, and will&lt;br&gt;
happily destroy your data), CHECK constraint changes, server defaults in many&lt;br&gt;
cases, and anything requiring data transformation.&lt;/p&gt;
&lt;p&gt;The generated file is a &lt;strong&gt;draft&lt;/strong&gt;. The comment Alembic itself writes says so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def upgrade() -&amp;gt; None:
    # ### commands auto generated by Alembic - please adjust! ###
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always read it. For anything beyond adding a column, expect to hand-write.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;trap-sqlite-cannot-alter-a-constraint&quot;&gt;Trap: SQLite cannot ALTER a constraint&lt;/h3&gt;
&lt;p&gt;Adding two values to an enum required widening a CHECK constraint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class ResourceType(StrEnum):
    EBS_VOLUME = &quot;ebs_volume&quot;
    ELASTIC_IP = &quot;elastic_ip&quot;
    EC2_INSTANCE = &quot;ec2_instance&quot;
    EBS_SNAPSHOT = &quot;ebs_snapshot&quot;
    RDS_INSTANCE = &quot;rds_instance&quot;    # new
    S3_BUCKET = &quot;s3_bucket&quot;          # new
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Postgres: &lt;code&gt;ALTER TABLE ... DROP CONSTRAINT ... ADD CONSTRAINT ...&lt;/code&gt;. Done.&lt;/p&gt;
&lt;p&gt;On SQLite: &lt;strong&gt;there is no ALTER for constraints.&lt;/strong&gt; The only route is create a new&lt;br&gt;
table, copy the data, drop the old, rename the new. Alembic wraps this in&lt;br&gt;
&lt;code&gt;batch_alter_table&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with op.batch_alter_table(&quot;resources&quot;, recreate=&quot;always&quot;) as batch_op:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;attempt-1-silently-kept-the-old-constraint&quot;&gt;Attempt 1: silently kept the old constraint&lt;/h4&gt;
&lt;p&gt;The first version passed &lt;code&gt;table_args&lt;/code&gt; and ran without error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with op.batch_alter_table(
    &quot;resources&quot;,
    recreate=&quot;always&quot;,
    table_args=(
        sa.CheckConstraint(_resource_type_check(NEW_TYPES), name=&quot;check_resource_type&quot;),
    ),
):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;alembic upgrade head&lt;/code&gt; printed success. Inserting an &lt;code&gt;rds_instance&lt;/code&gt; row still&lt;br&gt;
failed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code&gt;table_args&lt;/code&gt; &lt;em&gt;adds to&lt;/em&gt; a reflected table definition rather than&lt;br&gt;
replacing it. Alembic reflected the live table, including the old four-value&lt;br&gt;
CHECK, then appended the new six-value one. The rebuilt table had &lt;strong&gt;both&lt;/strong&gt;, and&lt;br&gt;
the old one still rejected every new row.&lt;/p&gt;
&lt;p&gt;Only dumping the schema revealed it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sqlite3 data/sentinel.db &quot;.schema resources&quot;
# CONSTRAINT check_resource_type CHECK (resource_type IN ('ebs_volume', ...))   ← old
# CONSTRAINT check_resource_type CHECK (resource_type IN ('ebs_volume', ..., 's3_bucket'))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;attempt-2-copy-from-to-suppress-reflection&quot;&gt;Attempt 2: &lt;code&gt;copy_from&lt;/code&gt; to suppress reflection&lt;/h4&gt;
&lt;p&gt;The fix is &lt;code&gt;copy_from&lt;/code&gt;, an explicit table definition that tells Alembic &lt;em&gt;not&lt;/em&gt;&lt;br&gt;
to reflect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _columns_only_resources() -&amp;gt; sa.Table:
    &quot;&quot;&quot;The `resources` table as columns and primary key, with NO constraints.

    Handed to batch_alter_table as `copy_from`, which suppresses reflection of
    the live table. That suppression is the point: `table_args` adds to a
    reflected definition rather than replacing it.
    &quot;&quot;&quot;
    return sa.Table(
        &quot;resources&quot;,
        sa.MetaData(),
        sa.Column(&quot;id&quot;, sa.String(), primary_key=True, nullable=False),
        sa.Column(&quot;resource_id&quot;, sa.String(), nullable=False),
        sa.Column(&quot;resource_type&quot;, sa.String(), nullable=False),
        sa.Column(&quot;resource_arn&quot;, sa.String(), nullable=False),
        sa.Column(&quot;region&quot;, sa.String(), nullable=False),
        sa.Column(&quot;current_tags&quot;, sa.String(), nullable=False),
        sa.Column(&quot;lifecycle&quot;, sa.String(), nullable=False),
        sa.Column(&quot;first_seen_at&quot;, sa.DateTime(timezone=True), nullable=False),
        sa.Column(&quot;last_seen_at&quot;, sa.DateTime(timezone=True), nullable=False),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;attempt-3-the-indexes-were-gone&quot;&gt;Attempt 3: the indexes were gone&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;copy_from&lt;/code&gt; suppresses &lt;strong&gt;index&lt;/strong&gt; reflection too. The rebuilt table lost its&lt;br&gt;
unique index on &lt;code&gt;resource_id&lt;/code&gt;, which the upsert logic depends on for&lt;br&gt;
correctness. So it gets recreated by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _rebuild_resources(resource_types: tuple[str, ...]) -&amp;gt; None:
    with op.batch_alter_table(
        &quot;resources&quot;,
        recreate=&quot;always&quot;,
        copy_from=_columns_only_resources(),
        table_args=(
            sa.CheckConstraint(&quot;lifecycle IN ('active', 'deleted')&quot;,
                               name=&quot;check_resource_lifecycle&quot;),
            sa.CheckConstraint(_resource_type_check(resource_types),
                               name=&quot;check_resource_type&quot;),
        ),
    ):
        pass  # the rebuild itself is the migration

    # copy_from also suppresses index reflection, so re-create by hand.
    bind = op.get_bind()
    existing = {index[&quot;name&quot;] for index in sa.inspect(bind).get_indexes(&quot;resources&quot;)}
    if &quot;ix_resources_resource_id&quot; not in existing:
        op.create_index(op.f(&quot;ix_resources_resource_id&quot;), &quot;resources&quot;,
                        [&quot;resource_id&quot;], unique=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rule for SQLite batch migrations: whatever the batch block omits is silently&lt;br&gt;
absent from the rebuilt table.&lt;/strong&gt; Every column, every constraint, every index has&lt;br&gt;
to be re-declared. And &quot;it ran without error&quot; proves nothing. Dump the schema.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;downgrades-that-refuse-to-run&quot;&gt;Downgrades that refuse to run&lt;/h3&gt;
&lt;p&gt;Most &lt;code&gt;downgrade()&lt;/code&gt; functions are written on autopilot as the inverse of&lt;br&gt;
&lt;code&gt;upgrade()&lt;/code&gt;. Sometimes the honest inverse is &lt;em&gt;refusing&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def downgrade() -&amp;gt; None:
    bind = op.get_bind()
    stranded = bind.execute(
        sa.text(&quot;SELECT COUNT(*) FROM resources &quot;
                &quot;WHERE resource_type IN ('rds_instance', 's3_bucket')&quot;)
    ).scalar_one()
    if stranded:
        raise RuntimeError(
            f&quot;Cannot downgrade: {stranded} resource(s) use rds_instance or s3_bucket, &quot;
            &quot;which the previous CHECK constraint forbids. Delete those resources and &quot;
            &quot;the findings referencing them first if this downgrade is intended.&quot;
        )
    _rebuild_resources(OLD_RESOURCE_TYPES)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Narrowing the constraint with those rows present means deleting them, which&lt;br&gt;
also orphans every finding referencing them. A downgrade that silently destroys&lt;br&gt;
data to succeed is worse than one that stops and explains.&lt;/p&gt;
&lt;p&gt;The docstring says so up front, so nobody is surprised at 2am:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;downgrade()&lt;/code&gt; restores the four-value constraint and will FAIL if any&lt;br&gt;
rds_instance or s3_bucket row exists by then, deliberately.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-simple-migration-for-contrast&quot;&gt;A simple migration, for contrast&lt;/h3&gt;
&lt;p&gt;Not every migration is a fight. Adding a table is exactly what you'd hope:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def upgrade() -&amp;gt; None:
    op.create_table(
        &quot;spend_snapshots&quot;,
        sa.Column(&quot;id&quot;, sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
        sa.Column(&quot;snapshot_date&quot;, sa.Date(), nullable=False),
        # Stored as a string, matching SafeNumeric: SQLite has no Decimal, and
        # a float column would quietly round money.
        sa.Column(&quot;total_estimated_monthly_usd&quot;, sa.String(), nullable=False),
        sa.Column(&quot;open_findings&quot;, sa.Integer(), nullable=False),
        sa.Column(&quot;active_resources&quot;, sa.Integer(), nullable=False),
        sa.Column(&quot;captured_at&quot;, sa.DateTime(timezone=True), nullable=False),
    )
    op.create_index(
        op.f(&quot;ix_spend_snapshots_snapshot_date&quot;),
        &quot;spend_snapshots&quot;, [&quot;snapshot_date&quot;], unique=True,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the docstring convention this project uses: the &lt;em&gt;why&lt;/em&gt;, not the &lt;em&gt;what&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;snapshot_date&lt;/code&gt; is UNIQUE on purpose. The repository upserts by date, so two&lt;br&gt;
scans on the same day collapse to one row. Otherwise a day that happened to&lt;br&gt;
be scanned five times would weigh five times as much in the trailing mean, and&lt;br&gt;
the z-score would measure scan cadence rather than spend.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Six months later, that paragraph is worth more than the code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;keeping-constraints-from-drifting&quot;&gt;Keeping constraints from drifting&lt;/h3&gt;
&lt;p&gt;CHECK constraints duplicating enum values is a drift hazard. This project&lt;br&gt;
generates them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _in_clause(column: str, values: type[StrEnum]) -&amp;gt; str:
    &quot;&quot;&quot;Render `column IN ('a', 'b', ...)` straight from a StrEnum.

    Generated rather than hand-written so the constraint cannot drift from the
    enum: adding a ResourceType now updates this automatically, and only the
    Alembic migration has to be written by hand.
    &quot;&quot;&quot;
    members = &quot;, &quot;.join(f&quot;'{member.value}'&quot; for member in values)
    return f&quot;{column} IN ({members})&quot;


class ResourceModel(Base):
    __tablename__ = &quot;resources&quot;
    ...
    __table_args__ = (
        CheckConstraint(_in_clause(&quot;lifecycle&quot;, ResourceLifecycle),
                        name=&quot;check_resource_lifecycle&quot;),
        CheckConstraint(_in_clause(&quot;resource_type&quot;, ResourceType),
                        name=&quot;check_resource_type&quot;),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model side can never drift from the enum. Only the migration is&lt;br&gt;
hand-written, which is unavoidable, since a migration must describe a &lt;em&gt;transition&lt;/em&gt; between&lt;br&gt;
two states, and only one of them exists in your code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;testing-migrations&quot;&gt;Testing migrations&lt;/h3&gt;
&lt;p&gt;A green &lt;code&gt;alembic upgrade head&lt;/code&gt; proves almost nothing (see attempt 1). Two things&lt;br&gt;
worth doing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Round-trip against a copy of a real database.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp data/sentinel.db /tmp/mig-test.db
SENTINEL_DB_PATH=/tmp/mig-test.db alembic upgrade head
SENTINEL_DB_PATH=/tmp/mig-test.db alembic downgrade -1
SENTINEL_DB_PATH=/tmp/mig-test.db alembic upgrade head

sqlite3 /tmp/mig-test.db &quot;select count(*) from resources; select count(*) from findings;&quot;
# 274 / 187, same as before
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real data, real row counts, and it catches data loss a fresh database never&lt;br&gt;
would.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Assert the error message is useful.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An un-migrated database should not produce a raw SQL traceback:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def upsert_resource(self, resource: Resource) -&amp;gt; None:
    try:
        self._upsert_resource(resource)
    except IntegrityError as exc:
        if &quot;check_resource_type&quot; not in str(exc):
            raise
        raise RuntimeError(
            f&quot;The findings database does not allow resource type &quot;
            f&quot;'{resource.resource_type}'. It was created by an older &quot;
            f&quot;version of FinOps Sentinel. Run `alembic upgrade head` &quot;
            f&quot;against {self.db_url} and scan again.&quot;
        ) from exc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failing hard is right: a half-written inventory is worse than a stopped run.&lt;br&gt;
But the traceback told nobody what to do. Now it names the type, the database,&lt;br&gt;
and the command.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;command-reference&quot;&gt;Command reference&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alembic revision -m &quot;message&quot;                  # empty migration, hand-written
alembic revision --autogenerate -m &quot;message&quot;   # diff models vs database

alembic upgrade head          # apply everything outstanding
alembic upgrade +1            # one step forward
alembic downgrade -1          # one step back
alembic downgrade base        # all the way down

alembic current               # what revision is this database at?
alembic history --verbose     # the full chain
alembic heads                 # &amp;gt;1 means you have a branch to merge
alembic merge -m &quot;merge&quot; a1b2 c3d4

alembic upgrade head --sql    # print SQL instead of executing (offline mode)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;alembic current&lt;/code&gt; is the one to reach for when something is confusing. It tells&lt;br&gt;
you where Alembic thinks the database is, and if that disagrees with the&lt;br&gt;
database you &lt;em&gt;expect&lt;/em&gt;, you have the bug from earlier in this post.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;checklist&quot;&gt;Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never edit a migration that has been applied anywhere but your laptop.&lt;/strong&gt;&lt;br&gt;
Write a new one. The old revision ID is a foreign key in every database that&lt;br&gt;
ran it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read autogenerated migrations before committing.&lt;/strong&gt; Especially for renames,&lt;br&gt;
which autogenerate turns into drop-plus-add.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the &quot;why&quot; in the docstring.&lt;/strong&gt; The SQL says what; only you can say why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test on a copy of real data.&lt;/strong&gt; Fresh databases hide data-loss bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dump the schema after non-trivial migrations.&lt;/strong&gt; &quot;It ran&quot; is not &quot;it worked.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give the database URL one owner&lt;/strong&gt; that both the app and Alembic call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make downgrades honest&lt;/strong&gt;, including refusing when the honest inverse is&lt;br&gt;
data loss.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Migrations are the one part of a codebase where a mistake is not just wrong but&lt;br&gt;
&lt;em&gt;permanently&lt;/em&gt; wrong, replicated to every environment that ran it. Worth the&lt;br&gt;
extra ten minutes.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Every example here is from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/boazleleina/finops-sentinel&quot;&gt;github.com/boazleleina/finops-sentinel&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26691/alembic-in-practice-structure-revisions-and-the-traps-sqlite-sets</guid>
<pubDate>Thu, 10 Sep 2026 03:50:15 +0000</pubDate>
</item>
<item>
<title>Shrinking a Materials Science Dataset</title>
<link>https://coderlegion.com/26483/shrinking-a-materials-science-dataset</link>
<description>&lt;p&gt;Shrinking datasets is quite contextual. It depends on how the dataset is going to be used. Its the same concept as optimizing databases, based on the read/write patterns.&lt;/p&gt;
&lt;p&gt;PACLEC and Traffic Patterns are the key determining factors.&lt;/p&gt;
&lt;p&gt;Hosted on fly.io : &lt;a rel=&quot;nofollow&quot; href=&quot;https://materials-db.fly.dev/&quot;&gt;https://materials-db.fly.dev/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The materials trajectory database was around 12Gigs and is mostly used for training models like CHGNet.&lt;br&gt;
But I needed it only for responding to query on finding materials and it didn't need all the variations.&lt;/p&gt;
&lt;p&gt;The aim was to be able to serve it using my most loved database - sqlite, in under reasonable timeframe: &amp;lt;500ms.&lt;/p&gt;
&lt;p&gt;Summary:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The source data and the problem.&lt;/strong&gt; The project is a Go API serving 145,923 materials from the Materials Project, backed by a single SQLite file. The source data is 11.35GB, while the database it actually runs on is only 266MB. The raw dataset (MPtrj) is so large because it was built as a training corpus for a machine-learning model - each material's relaxation run gets sampled at several points along a trajectory at different pressures and energies, making every point a valid training example. That's why the dataset has 1.58 million rows, not because of sloppy deduplication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KeySpace Reduction&lt;/strong&gt;: Most of the times JSON has long key names, this is unnecessary to storage, so converting keys like &quot;lattice_structure&quot; to &quot;ls&quot; was the first step. This was the initial memory optimization that was needed. The versioned response adapter would expand these back&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The big win: deduplication (~20x reduction).&lt;/strong&gt; The first and largest cut came purely from scope - the ingest step keeps only one structure per material, picked by lowest energy, using a &lt;code&gt;ROW_NUMBER()&lt;/code&gt; window function. Every other task retains only five scalar properties in a separate table, but loses its full structure blob. This single decision takes the dataset from 11.35GB down to roughly 592MB. This isn't &quot;free cleanup&quot; - those discarded structures had real scientific value for ML training, but the API's job (ground-state lookups) simply didn't need them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compression: 592MB -&amp;gt; 266MB.&lt;/strong&gt; The remaining bulk was almost entirely &lt;code&gt;structure_json&lt;/code&gt; blobs - one per material. Two techniques were applied: first, the verbose pymatgen JSON format was stripped down to a compact positional array (lattice matrix + sites as &lt;code&gt;[element, a, b, c, occupancy]&lt;/code&gt;), and then zlib compression was applied on top. Benchmarking showed that stripping first and then compressing outperformed either approach alone. Each blob also got a 4-byte versioned header so old and new rows could coexist during migration without requiring a full re-ingest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Serving it fast.&lt;/strong&gt; At 266MB, the entire database fits inside a single 1GB mmap region, so reads hit the page cache after the first touch. A channel-based pool of read-only connections runs warmup queries against key indexes on boot, singleflight collapses concurrent identical requests into one SQLite call, and an LRU cache sits in front of lookups since the data is static. The honest caveat: a cold start on a small VM still takes 15–24 seconds for the first request, but everything after that is sub-500ms. Browser cache headers do the rest of the job&lt;/p&gt;
&lt;p&gt;Apart from singleflight, nginx could also be used, but this is fast enough.&lt;/p&gt;
&lt;p&gt;A detailed write up is provided in my blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ikouchiha47.github.io/2026/08/05/materialsdb-11gb-to-266mb.html&quot;&gt;https://ikouchiha47.github.io/2026/08/05/materialsdb-11gb-to-266mb.html&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26483/shrinking-a-materials-science-dataset</guid>
<pubDate>Tue, 08 Sep 2026 11:37:32 +0000</pubDate>
</item>
<item>
<title>Ranking New Papers By Meaning Instead Of Keywords, In One PHP App</title>
<link>https://coderlegion.com/25197/ranking-new-papers-by-meaning-instead-of-keywords-in-one-php-app</link>
<description>&lt;p&gt;Most of us have built some version of a scheduled job that watches a feed and mails out a summary, and it almost never fails on the plumbing. It fails on relevance, and this is a writeup of a small PHP and SQLite project that puts a model scoring step where the keyword filter usually goes.&lt;/p&gt;
&lt;h3 id=&quot;why-keyword-alerts-keep-missing-the-right-paper&quot;&gt;Why Keyword Alerts Keep Missing The Right Paper&lt;/h3&gt;
&lt;p&gt;Every major index offers alerts, and they all match on words. That is the whole problem. A paper that applies your exact method in a neighboring field will share none of your keywords, so it never reaches you. A paper that happens to use your words while solving an unrelated problem arrives every single week.&lt;/p&gt;
&lt;p&gt;Word overlap is a proxy for relevance, and it is a bad one the moment your work sits between two subfields. Any watcher you build on top of a keyword query inherits that ceiling no matter how good the rest of the code is.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-the-pipeline&quot;&gt;The Shape Of The Pipeline&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learnhowtoscience.com/ai-science-tools/literature-radar.php&quot;&gt;Literature Radar&lt;/a&gt; splits the job into four steps, and the split is the interesting part.&lt;/p&gt;
&lt;p&gt;First, an interest profile. Not a keyword list, a short paragraph written the way you would describe your work to a colleague. Separately you define topic queries that tell the poller what to pull, so retrieval stays broad while ranking stays narrow.&lt;/p&gt;
&lt;p&gt;Second, a scheduled poll against arXiv, PubMed, Crossref, bioRxiv and medRxiv, writing into one local SQLite file. Anything already seen is skipped, so the table doubles as the dedupe layer.&lt;/p&gt;
&lt;p&gt;Third, the ranking pass. Your model reads each new title and abstract and scores it 0 to 10 against the profile, attaching a one line reason. This is where a paper about your method in another field can outscore a paper that matched your keywords exactly.&lt;/p&gt;
&lt;p&gt;Fourth, the digest. One email on your schedule, best first, score and reason attached, nothing repeated across sends.&lt;/p&gt;
&lt;h3 id=&quot;choosing-where-the-ranking-runs&quot;&gt;Choosing Where The Ranking Runs&lt;/h3&gt;
&lt;p&gt;The ranking backend is resolved in order: the Claude command line tool, the Codex command line tool, an Anthropic API key, then any OpenAI compatible endpoint, which covers OpenAI, OpenRouter, and local servers like Ollama or LM Studio.&lt;/p&gt;
&lt;p&gt;That order matters for cost. If you already pay for a Claude or ChatGPT subscription, the CLI path means the ranking step adds no per token cost at all, which is what makes a daily job over a few hundred abstracts practical for one person. Falling back to a local model through an OpenAI compatible endpoint keeps abstracts on your own machine entirely.&lt;/p&gt;
&lt;p&gt;The whole thing is one PHP app plus one SQLite file, driven by a single cron entry that decides internally whether a poll, a rank, or a digest is due:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 */2 * * * php /path/to/Literature-Radar/cli/cron.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each step also runs standalone, which is what you want while tuning the profile.&lt;/p&gt;
&lt;h3 id=&quot;what-transfers-to-your-own-watcher&quot;&gt;What Transfers To Your Own Watcher&lt;/h3&gt;
&lt;p&gt;Three things generalize past papers. Keep retrieval broad and put the judgment in a separate ranking pass, because a narrow query loses items you can never recover later. Store the reason next to the score, since a ranked list with no explanation gives you nothing to tune against. And make the scoring backend swappable from day one, because the model you start on will not be the one you finish on.&lt;/p&gt;
&lt;p&gt;It is MIT licensed with no account and no service in the middle, so the abstracts go to whichever model you picked and nowhere else.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25197/ranking-new-papers-by-meaning-instead-of-keywords-in-one-php-app</guid>
<pubDate>Sun, 23 Aug 2026 22:05:46 +0000</pubDate>
</item>
<item>
<title>One PHP App and One SQLite File: A Self Hosted Workflow Engine</title>
<link>https://coderlegion.com/24640/one-php-app-and-one-sqlite-file-a-self-hosted-workflow-engine</link>
<description>&lt;p&gt;Most of us here have written the same glue script more than once, the one that catches a form submission and pushes it somewhere else, then quietly rots in a cron job nobody documented. This is a look at an open source engine that gives that glue a proper home on your own server, written for anyone who has priced a hosted automation tool and done the math.&lt;/p&gt;
&lt;h3 id=&quot;what-it-actually-is&quot;&gt;What It Actually Is&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.aiappsapi.com/Workflow-Chain-Engine/&quot;&gt;Workflow Chain Engine&lt;/a&gt; is a visual workflow automation engine you host yourself. You draw a flow in the browser, call an API, branch on the answer, loop over a list, and end in success or fail. The whole system is one small PHP application with a single SQLite file. No framework, no build step, no dependency install, and the editor library ships inside the project, so nothing is loaded from a CDN at runtime.&lt;/p&gt;
&lt;p&gt;That constraint is the interesting part. It means the tool is at home on a three dollar shared host, a VPS, or a Docker container, which matters when the process you are automating is worth ten dollars a month rather than two hundred.&lt;/p&gt;
&lt;h3 id=&quot;the-parts-that-matter-in-practice&quot;&gt;The Parts That Matter in Practice&lt;/h3&gt;
&lt;p&gt;Six node types cover the logic you need: calling out, branching, looping, and terminating in a success or fail state. Variables carry data between steps, so the response from one API call is available to the next node without writing any glue of your own to move it there.&lt;/p&gt;
&lt;p&gt;Triggers come in four flavors: a webhook, a schedule, a call from your own code, or a button someone clicks. That last one is underrated. A lot of internal automation is really a task somebody runs by hand at an unpredictable time, and handing that person a button beats handing them a runbook.&lt;/p&gt;
&lt;h3 id=&quot;the-run-log-is-the-feature&quot;&gt;The Run Log Is the Feature&lt;/h3&gt;
&lt;p&gt;Every run leaves a step by step log you can actually read. That sounds mundane right up until you have debugged a hosted automation whose entire error report was the phrase workflow failed. Knowing which node fired, what it received, and what it returned is the difference between a five minute fix and a lost afternoon.&lt;/p&gt;
&lt;p&gt;Loop protection is built in as well, which is the other thing you want in place before you point a webhook at something capable of calling itself.&lt;/p&gt;
&lt;h3 id=&quot;worth-a-look-if&quot;&gt;Worth a Look If&lt;/h3&gt;
&lt;p&gt;You are already self hosting the database and the object storage but still renting the glue. The code is MIT licensed and on GitHub, so the cost of finding out is one afternoon.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24640/one-php-app-and-one-sqlite-file-a-self-hosted-workflow-engine</guid>
<pubDate>Fri, 14 Aug 2026 16:23:47 +0000</pubDate>
</item>
<item>
<title>From GitHub Repositories to a Professional Data Analytics Portfolio Website</title>
<link>https://coderlegion.com/24288/from-github-repositories-to-a-professional-data-analytics-portfolio-website</link>
<description>&lt;h3 id=&quot;building-more-than-just-projects-building-my-professional-identity&quot;&gt;Building More Than Just Projects — Building My Professional Identity&lt;/h3&gt;
&lt;p&gt;Completing my &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;Data Analyst Internship&lt;/a&gt;&lt;/strong&gt; at &lt;strong&gt;Logic Stack&lt;/strong&gt; was a significant milestone. Over four weeks, I worked on real-world business problems using &lt;strong&gt;Excel, Power BI, Python and SQL&lt;/strong&gt;, successfully completing four practical analytics projects.&lt;/p&gt;
&lt;p&gt;Like many learners, I initially thought that uploading my projects to GitHub would be enough.&lt;/p&gt;
&lt;p&gt;After all, GitHub is the standard platform for developers and data professionals.&lt;/p&gt;
&lt;p&gt;But then I asked myself a simple question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;If a recruiter visits my profile, will they truly understand my journey just by opening four separate repositories?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer was &lt;strong&gt;No.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That realization inspired me to build something much bigger than another GitHub repository.&lt;/p&gt;
&lt;p&gt;It inspired me to build my own &lt;strong&gt;Production-Level Data Analytics &lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;Portfolio Website&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-github-alone-wasn-t-enough&quot;&gt;Why GitHub Alone Wasn't Enough&lt;/h2&gt;
&lt;p&gt;GitHub is excellent for storing source code and documentation.&lt;/p&gt;
&lt;p&gt;However, recruiters usually spend only a few minutes reviewing a candidate's profile.&lt;/p&gt;
&lt;p&gt;Opening multiple repositories one by one makes it difficult to understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The overall learning journey&lt;/li&gt;
&lt;li&gt;Project progression&lt;/li&gt;
&lt;li&gt;Skills gained&lt;/li&gt;
&lt;li&gt;Business impact&lt;/li&gt;
&lt;li&gt;Technical growth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to create a place where everything could be explored from a &lt;strong&gt;single professional dashboard&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-idea&quot;&gt;The Idea&lt;/h2&gt;
&lt;p&gt;Instead of sharing multiple independent repositories, I designed a portfolio website that presents my internship as one complete journey.&lt;/p&gt;
&lt;p&gt;Rather than saying,&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;Here are my projects.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wanted the website to tell a story:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How I started, what I learned, which challenges I faced, how I solved them and what I built by the end of the internship.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That became the foundation of the entire portfolio.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;designing-the-portfolio&quot;&gt;Designing the Portfolio&lt;/h2&gt;
&lt;p&gt;The goal was simple:&lt;/p&gt;
&lt;p&gt;Create a clean, modern, recruiter-friendly website that highlights both my &lt;strong&gt;Data Analytics skills&lt;/strong&gt; and my &lt;strong&gt;Web Development experience&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The portfolio includes carefully designed sections that guide visitors through my internship journey.&lt;/p&gt;
&lt;h4 id=&quot;home&quot;&gt;Home&lt;/h4&gt;
&lt;p&gt;A professional landing page introducing my internship, background, and career focus.&lt;/p&gt;
&lt;h4 id=&quot;projects&quot;&gt;Projects&lt;/h4&gt;
&lt;p&gt;A dedicated section for all four internship projects.&lt;/p&gt;
&lt;p&gt;Each project contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project overview&lt;/li&gt;
&lt;li&gt;Technologies used&lt;/li&gt;
&lt;li&gt;GitHub Repository&lt;/li&gt;
&lt;li&gt;LinkedIn post&lt;/li&gt;
&lt;li&gt;YouTube explanation video&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything is connected from one location.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;skills-learned&quot;&gt;Skills Learned&lt;/h2&gt;
&lt;p&gt;Instead of simply listing technologies, I organized everything I learned during the internship into one dedicated section.&lt;/p&gt;
&lt;p&gt;The portfolio highlights skills including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft Excel&lt;/li&gt;
&lt;li&gt;Power BI&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Data Cleaning&lt;/li&gt;
&lt;li&gt;Exploratory Data Analysis&lt;/li&gt;
&lt;li&gt;KPI Development&lt;/li&gt;
&lt;li&gt;Business Intelligence&lt;/li&gt;
&lt;li&gt;Dashboard Design&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives recruiters a quick overview of my technical capabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenges-solved&quot;&gt;Challenges Solved&lt;/h2&gt;
&lt;p&gt;One of my favourite sections is the &lt;strong&gt;Challenges Solved&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;Instead of hiding the problems I encountered, I documented them professionally.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python environment setup&lt;/li&gt;
&lt;li&gt;Library installation issues&lt;/li&gt;
&lt;li&gt;SQL query debugging&lt;/li&gt;
&lt;li&gt;Data cleaning problems&lt;/li&gt;
&lt;li&gt;Documentation challenges&lt;/li&gt;
&lt;li&gt;GitHub repository organization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each challenge is paired with the solution I implemented.&lt;/p&gt;
&lt;p&gt;I believe problem-solving demonstrates professional growth more effectively than simply showing successful projects.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;certificate-amp-internship-documents&quot;&gt;Certificate &amp;amp; Internship Documents&lt;/h2&gt;
&lt;p&gt;The portfolio also includes my internship certificate and professional documents.&lt;/p&gt;
&lt;p&gt;To keep them organized, I implemented a simple password-protected section that allows authorized viewers to access these files when needed.&lt;/p&gt;
&lt;p&gt;This keeps the homepage clean while still making important documents available.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;building-with-modern-technologies&quot;&gt;Building with Modern Technologies&lt;/h2&gt;
&lt;p&gt;This project also became an opportunity to strengthen my web development skills.&lt;/p&gt;
&lt;p&gt;The website was built using modern technologies including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Framer Motion&lt;/li&gt;
&lt;li&gt;Responsive Design&lt;/li&gt;
&lt;li&gt;SEO Optimization&lt;/li&gt;
&lt;li&gt;Structured Data (JSON-LD)&lt;/li&gt;
&lt;li&gt;Robots.txt&lt;/li&gt;
&lt;li&gt;Sitemap.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By combining these technologies, I transformed a simple portfolio into a production-ready web application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;seo-matters-too&quot;&gt;SEO Matters Too&lt;/h2&gt;
&lt;p&gt;I didn't want the website to look professional only for people who already had the link.&lt;/p&gt;
&lt;p&gt;I wanted search engines to understand it as well.&lt;/p&gt;
&lt;p&gt;To improve visibility, I implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Metadata optimization&lt;/li&gt;
&lt;li&gt;Open Graph tags&lt;/li&gt;
&lt;li&gt;Structured Data&lt;/li&gt;
&lt;li&gt;Robots configuration&lt;/li&gt;
&lt;li&gt;XML Sitemap&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Achieving high Lighthouse scores became another goal during development, ensuring the website follows modern web standards.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;combining-two-different-skill-sets&quot;&gt;Combining Two Different Skill Sets&lt;/h2&gt;
&lt;p&gt;This project became much more than a portfolio.&lt;/p&gt;
&lt;p&gt;It combined two different fields I enjoy:&lt;/p&gt;
&lt;h4 id=&quot;data-analytics&quot;&gt;Data Analytics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Business insights&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Power BI&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Excel&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;web-development&quot;&gt;Web Development&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;UI Design&lt;/li&gt;
&lt;li&gt;Responsive Layouts&lt;/li&gt;
&lt;li&gt;SEO&lt;/li&gt;
&lt;li&gt;Performance Optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bringing these two disciplines together allowed me to create something that represents my complete technical profile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-beyond-the-internship&quot;&gt;Lessons Beyond the Internship&lt;/h2&gt;
&lt;p&gt;Building this website taught me several important lessons.&lt;/p&gt;
&lt;p&gt;A portfolio is not just about displaying work.&lt;/p&gt;
&lt;p&gt;It is about presenting your journey professionally.&lt;/p&gt;
&lt;p&gt;I learned that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Presentation matters.&lt;/li&gt;
&lt;li&gt;Documentation matters.&lt;/li&gt;
&lt;li&gt;User experience matters.&lt;/li&gt;
&lt;li&gt;Personal branding matters.&lt;/li&gt;
&lt;li&gt;Professional storytelling matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A recruiter should be able to understand who you are within minutes.&lt;/p&gt;
&lt;p&gt;That became the guiding principle behind this project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Looking back, I'm glad I didn't stop after uploading four repositories.&lt;/p&gt;
&lt;p&gt;Creating this portfolio allowed me to organize my internship experience into one professional platform that showcases both my technical skills and my continuous learning journey.&lt;/p&gt;
&lt;p&gt;This website represents more than projects.&lt;/p&gt;
&lt;p&gt;It represents persistence.&lt;/p&gt;
&lt;p&gt;It represents growth.&lt;/p&gt;
&lt;p&gt;It represents every challenge solved throughout the internship.&lt;/p&gt;
&lt;p&gt;Most importantly, it reminds me that learning never truly ends.&lt;/p&gt;
&lt;p&gt;Every project becomes a stepping stone toward the next opportunity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;explore-my-complete-portfolio&quot;&gt;Explore My Complete Portfolio&lt;/h3&gt;
&lt;p&gt;🌐 &lt;strong&gt;Live Portfolio:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;https://yasirawaninfo-data.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831&quot;&gt;https://github.com/YasirAwan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for taking the time to explore my journey. I hope it inspires other learners to build not only projects—but also a professional identity around their work.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;#DataAnalytics #SQL #SQLite #PowerBI #Python #Panda  #Numpy #Matplotlib #Excel #BusinessIntelligence  #DataVisualization #Dashboard #DataCleaning #JupyterNotebook #ProblemSolving  #GitHub #WebDevelopment #LearningInPublic #Portfolio #CareerGrowth ##MuhammadYasir #YasirAwan4831&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24288/from-github-repositories-to-a-professional-data-analytics-portfolio-website</guid>
<pubDate>Sat, 08 Aug 2026 07:35:02 +0000</pubDate>
</item>
<item>
<title>Continuing My Data Analyst Journey: Week 3, 4 and Challenges I Faced During My Data Analyst Intern</title>
<link>https://coderlegion.com/24267/continuing-my-data-analyst-journey-week-and-challenges-faced-during-data-analyst-intern</link>
<description>&lt;p&gt;In my previous blog, I shared my journey through Week 1 and Week 2, where I worked on Excel-based data cleaning, sales analysis, Pivot Tables and Power BI dashboards.&lt;/p&gt;
&lt;p&gt;As the internship progressed, the learning curve became much steeper. The focus shifted from spreadsheet analytics to programming, databases and solving real-world business problems using professional data analysis tools.&lt;/p&gt;
&lt;p&gt;Weeks 3 and 4 introduced completely new technologies, larger datasets and more technical challenges that pushed me far beyond my comfort zone.&lt;/p&gt;
&lt;p&gt;These two weeks transformed my internship from learning business reporting into building complete end-to-end data analytics solutions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;week-3-supply-chain-analytics-using-python-amp-power-bi&quot;&gt;Week 3: Supply Chain Analytics using Python &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;Week 3 marked my first serious experience with &lt;strong&gt;Python for Data Analytics.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Unlike previous weeks, this project required setting up a complete Python development environment before writing a single line of analysis. I worked extensively with &lt;strong&gt;Pandas, NumPy, Matplotlib and Jupyter Notebook&lt;/strong&gt; to clean, analyse and visualize a real-world supply chain dataset containing thousands of records.&lt;/p&gt;
&lt;p&gt;After completing the &lt;strong&gt;Exploratory Ddata Analysis (EDA)&lt;/strong&gt;, I integrated the insights into &lt;strong&gt;Power BI&lt;/strong&gt;, creating an interactive dashboard that highlighted sales performance, shipment modes, product groups, vendors and country-wise business metrics.&lt;/p&gt;
&lt;p&gt;This project taught me that successful data analysis begins with clean, reliable data—and that Python is one of the most powerful tools for transforming raw datasets into meaningful business insights.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt;  &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&quot;&gt;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YouTube Video:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/SQk41XxxpsY?si=ElhxePDt916bh_VJ&quot;&gt;https://youtu.be/SQk41XxxpsY?si=ElhxePDt916bh_VJ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedIn Post:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_eda-dataabranalyst-fullabrstackabrdeveloper-activity-7482280841147006976-YKho?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_eda-dataabranalyst-fullabrstackabrdevel&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;week-4-funnel-amp-revenue-analysis-using-sql-amp-power-bi&quot;&gt;Week 4: Funnel &amp;amp; Revenue Analysis using SQL &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;The final week introduced one of the most important skills in modern data analytics: &lt;strong&gt;SQL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For this project, I worked with SQLite to analyse customer funnel performance, revenue generation and conversion metrics directly from a relational database. Instead of relying on spreadsheets, I learned how businesses retrieve insights using SQL queries, aggregate functions, filtering, grouping and KPI calculations.&lt;/p&gt;
&lt;p&gt;The extracted data was then connected to &lt;strong&gt;Power BI&lt;/strong&gt;, where I built an interactive business dashboard presenting conversion rates, revenue trends and overall funnel performance.&lt;/p&gt;
&lt;p&gt;This project completed my internship journey by combining database querying, business intelligence and visualization into one complete analytics workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;:  &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&quot;&gt;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YouTube Video:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/sdBYOKqT6YM?si=HfNw1CrgG71dHIzi&quot;&gt;https://youtu.be/sdBYOKqT6YM?si=HfNw1CrgG71dHIzi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedIn Post:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_sqlite-sqlabrbased-dataabranalyst-activity-7486642277780742144-lXpI?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_sqlite-sqlabrbased-dataabranalys&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-learning-tools-to-solving-problems&quot;&gt;From Learning Tools to Solving Problems&lt;/h3&gt;
&lt;p&gt;By the end of Week 4, I had worked with:&lt;br&gt;
Microsoft Excel&lt;br&gt;
Power BI&lt;br&gt;
Python&lt;br&gt;
SQL&lt;br&gt;
Business Analytics&lt;br&gt;
Data Cleaning&lt;br&gt;
Exploratory Data Analysis (EDA)&lt;br&gt;
KPI Dashboard Development&lt;br&gt;
Funnel Analysis&lt;br&gt;
GitHub Documentation&lt;/p&gt;
&lt;p&gt;However, learning these tools was only part of the journey.&lt;br&gt;
The real growth came from overcoming technical challenges, debugging errors, researching solutions and continuously improving every project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-real-challenges-i-faced-during-my-data-analyst-internship-and-how-i-solved-them-&quot;&gt;10 Real Challenges I Faced During My Data Analyst Internship (And How I Solved Them)&lt;/h2&gt;
&lt;p&gt; Every Internship Teaches Skills — But Real Growth Comes From Solving Problems&lt;/p&gt;
&lt;p&gt;When people look at a completed project, they usually see the dashboard, the charts, the SQL queries, or the final GitHub repository.&lt;/p&gt;
&lt;p&gt;What they don't see are the countless hours spent fixing errors, learning unfamiliar tools, debugging environments, researching documentation and solving problems that never appeared in any tutorial.&lt;/p&gt;
&lt;p&gt;During my one-month Data Analyst Internship at &lt;strong&gt;Logic Stack&lt;/strong&gt;, I completed four industry-based projects covering &lt;strong&gt;Excel, Power BI, Python, SQL and Business Analytics&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;However, the most valuable outcome wasn't just the projects themselves.&lt;/p&gt;
&lt;p&gt;It was learning &lt;strong&gt;how to solve problems independently.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This blog shares the biggest technical and practical challenges I faced during my internship—and how each one made me a better Data Analyst.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-3-project&quot;&gt;Week 3 Project&lt;/h2&gt;
&lt;h3 id=&quot;supply-chain-analytics-using-python-amp-power-bi&quot;&gt;Supply Chain Analytics using Python &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;By Week 3, the internship shifted from Excel into Python-based analytics.&lt;/p&gt;
&lt;p&gt;This project focused on cleaning, analysing and visualizing a real-world Supply Chain dataset using &lt;strong&gt;Python (Pandas, NumPy, Matplotlib)&lt;/strong&gt; alongside &lt;strong&gt;Power BI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Unlike previous weeks, this project introduced a completely new development environment, making it both exciting and challenging.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-4-project&quot;&gt;Week 4 Project&lt;/h2&gt;
&lt;h3 id=&quot;funnel-amp-revenue-analysis-using-sql-amp-power-bi&quot;&gt;Funnel &amp;amp; Revenue Analysis using SQL &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;The final week introduced SQL for business analytics.&lt;/p&gt;
&lt;p&gt;The objective was to analyse conversion funnels, revenue performance and customer behaviour using SQL queries before presenting business insights inside Power BI dashboards.&lt;/p&gt;
&lt;p&gt;This project simulated real-world reporting workflows where raw database queries become executive dashboards.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-1&quot;&gt;Challenge 1&lt;/h2&gt;
&lt;h3 id=&quot;starting-with-zero-practical-experience&quot;&gt;Starting With Zero Practical Experience&lt;/h3&gt;
&lt;h4 id=&quot;challenge&quot;&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Although I had experience in web development, I had almost no practical exposure to professional Data Analytics workflows.&lt;/p&gt;
&lt;p&gt;Everything—from Excel reporting to SQL analytics—was new.&lt;/p&gt;
&lt;h4 id=&quot;solution&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of trying to learn everything at once, I followed a structured approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis&quot;&gt;Week 1 → Excel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-2-excel-powerbi-sales-dashboard&quot;&gt;Week 2 → Power BI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&quot;&gt;Week 3 → Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&quot;&gt;Week 4 → SQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Learning one technology at a time helped me build confidence while creating portfolio-ready projects every week.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-2&quot;&gt;Challenge 2&lt;/h2&gt;
&lt;h3 id=&quot;building-a-professional-excel-workflow&quot;&gt;Building a Professional Excel Workflow&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Creating clean spreadsheets is easy.&lt;/p&gt;
&lt;p&gt;Creating professional business reports is not.&lt;/p&gt;
&lt;p&gt;I had to learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured Tables&lt;/li&gt;
&lt;li&gt;Pivot Tables&lt;/li&gt;
&lt;li&gt;Professional Formatting&lt;/li&gt;
&lt;li&gt;Multiple Worksheets&lt;/li&gt;
&lt;li&gt;Formula Organization&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-1&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I researched Excel best practices and redesigned my workbook several times until it followed professional reporting standards.&lt;/p&gt;
&lt;p&gt;The result was an organized multi-sheet Excel project that later became the foundation for my Power BI dashboard.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-3&quot;&gt;Challenge 3&lt;/h2&gt;
&lt;h3 id=&quot;learning-power-bi-dashboard-design&quot;&gt;Learning Power BI Dashboard Design&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Power BI wasn't difficult because of the software.&lt;/p&gt;
&lt;p&gt;It was difficult because dashboards must answer business questions—not simply display charts.&lt;/p&gt;
&lt;h4 id=&quot;solution-2&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I learned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KPI Cards&lt;/li&gt;
&lt;li&gt;DAX Basics&lt;/li&gt;
&lt;li&gt;Dashboard Layout Principles&lt;/li&gt;
&lt;li&gt;Visual Hierarchy&lt;/li&gt;
&lt;li&gt;Business Storytelling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than creating colourful visuals, I focused on dashboards that communicate insights clearly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-4&quot;&gt;Challenge 4&lt;/h2&gt;
&lt;h3 id=&quot;configuring-the-python-development-environment&quot;&gt;Configuring the Python Development Environment&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;This was one of the toughest technical challenges.&lt;/p&gt;
&lt;p&gt;Issues included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VS Code Python interpreter problems&lt;/li&gt;
&lt;li&gt;Jupyter Kernel connection failures&lt;/li&gt;
&lt;li&gt;Package installation errors&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;Version conflicts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These problems had nothing to do with analytics—but they completely blocked development.&lt;/p&gt;
&lt;h4 id=&quot;solution-3&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of giving up, I researched official documentation, debugging guides and community discussions.&lt;/p&gt;
&lt;p&gt;Eventually I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed interpreter paths&lt;/li&gt;
&lt;li&gt;Installed required libraries&lt;/li&gt;
&lt;li&gt;Connected Jupyter successfully&lt;/li&gt;
&lt;li&gt;Stabilized the development environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This experience taught me that environment setup is a real engineering skill.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-5&quot;&gt;Challenge 5&lt;/h2&gt;
&lt;h3 id=&quot;cleaning-a-real-world-dataset&quot;&gt;Cleaning a Real-World Dataset&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;The Supply Chain dataset contained:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing values&lt;/li&gt;
&lt;li&gt;Mixed data types&lt;/li&gt;
&lt;li&gt;Invalid numeric values&lt;/li&gt;
&lt;li&gt;Text inside number columns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Real business datasets are rarely clean.&lt;/p&gt;
&lt;h4 id=&quot;solution-4&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Using Pandas, I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converted invalid values safely&lt;/li&gt;
&lt;li&gt;Filled missing values&lt;/li&gt;
&lt;li&gt;Removed inconsistencies&lt;/li&gt;
&lt;li&gt;Validated cleaned data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only after proper cleaning did visualization become meaningful.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-6&quot;&gt;Challenge 6&lt;/h2&gt;
&lt;h3 id=&quot;installing-sqlite-amp-building-sql-workflows&quot;&gt;Installing SQLite &amp;amp; Building SQL Workflows&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Week 4 introduced SQL.&lt;/p&gt;
&lt;p&gt;Before writing queries, I first had to configure SQLite and import datasets correctly.&lt;/p&gt;
&lt;h4 id=&quot;solution-5&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I installed DB Browser for SQLite, verified database structure, imported CSV datasets correctly and prepared the environment before beginning SQL analysis.&lt;/p&gt;
&lt;p&gt;This preparation prevented many future errors.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-7&quot;&gt;Challenge 7&lt;/h2&gt;
&lt;h3 id=&quot;writing-complex-sql-queries&quot;&gt;Writing Complex SQL Queries&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Simple SELECT statements were easy.&lt;/p&gt;
&lt;p&gt;Business reporting required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GROUP BY&lt;/li&gt;
&lt;li&gt;Aggregate Functions&lt;/li&gt;
&lt;li&gt;Filtering&lt;/li&gt;
&lt;li&gt;Revenue Calculations&lt;/li&gt;
&lt;li&gt;Conversion Metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-6&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of memorizing syntax, I practiced writing increasingly complex queries while documenting each command inside my repository.&lt;/p&gt;
&lt;p&gt;This improved both my SQL knowledge and documentation skills.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-8&quot;&gt;Challenge 8&lt;/h2&gt;
&lt;h3 id=&quot;turning-data-into-business-insights&quot;&gt;Turning Data Into Business Insights&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Anyone can generate numbers.&lt;/p&gt;
&lt;p&gt;A Data Analyst must explain what those numbers actually mean.&lt;/p&gt;
&lt;h4 id=&quot;solution-7&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;For every project I focused on answering business questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which products perform best?&lt;/li&gt;
&lt;li&gt;Where is revenue growing?&lt;/li&gt;
&lt;li&gt;Which funnel stage loses customers?&lt;/li&gt;
&lt;li&gt;What actions should businesses take?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This mindset transformed technical work into business analysis.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-9&quot;&gt;Challenge 9&lt;/h2&gt;
&lt;h3 id=&quot;professional-github-documentation&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis/blob/main/README.md&quot;&gt;Professional GitHub Documentation&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Writing code is only half the job.&lt;/p&gt;
&lt;p&gt;Presenting &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis/blob/main/README.md&quot;&gt;projects professionally&lt;/a&gt; is equally important.&lt;/p&gt;
&lt;h4 id=&quot;solution-8&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I created consistent repositories with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Detailed README files&lt;/li&gt;
&lt;li&gt;Professional documentation&lt;/li&gt;
&lt;li&gt;SVG animations&lt;/li&gt;
&lt;li&gt;Shields.io badges&lt;/li&gt;
&lt;li&gt;Folder organization&lt;/li&gt;
&lt;li&gt;Screenshots&lt;/li&gt;
&lt;li&gt;Business explanations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These repositories became portfolio-quality projects rather than simple assignments.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-10&quot;&gt;Challenge 10&lt;/h2&gt;
&lt;h3 id=&quot;building-my-own-data-analyst-portfolio-website&quot;&gt;Building My Own Data Analyst Portfolio Website&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Instead of simply uploading four GitHub repositories, I wanted to create something unique.&lt;/p&gt;
&lt;h4 id=&quot;solution-9&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I designed and developed a complete interactive portfolio website using &lt;strong&gt;Next.js&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The website includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internship Overview&lt;/li&gt;
&lt;li&gt;Four Complete Projects&lt;/li&gt;
&lt;li&gt;GitHub Repositories&lt;/li&gt;
&lt;li&gt;LinkedIn Posts&lt;/li&gt;
&lt;li&gt;YouTube Project Videos&lt;/li&gt;
&lt;li&gt;Skills Learned&lt;/li&gt;
&lt;li&gt;Challenges Solved&lt;/li&gt;
&lt;li&gt;Password-Protected Certificate Section&lt;/li&gt;
&lt;li&gt;Resume Download&lt;/li&gt;
&lt;li&gt;Modern Responsive UI&lt;/li&gt;
&lt;li&gt;SEO Optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This project combined my web development background with my newly acquired Data Analytics skills, creating a portfolio that reflects both technical implementation and professional presentation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-this-internship-really-taught-me&quot;&gt;What This Internship Really Taught Me&lt;/h2&gt;
&lt;p&gt;This internship wasn't just about learning Excel, Python, SQL, or Power BI.&lt;/p&gt;
&lt;p&gt;It taught me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to research independently.&lt;/li&gt;
&lt;li&gt;How to debug unfamiliar problems.&lt;/li&gt;
&lt;li&gt;How to organize professional projects.&lt;/li&gt;
&lt;li&gt;How to document technical work.&lt;/li&gt;
&lt;li&gt;How to communicate business insights.&lt;/li&gt;
&lt;li&gt;How to keep learning when solutions aren't immediately obvious.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These lessons will remain valuable long after the internship ends.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Looking back, the dashboards and repositories represent only the visible outcome.&lt;/p&gt;
&lt;p&gt;The real achievement was overcoming every obstacle that appeared along the way.&lt;/p&gt;
&lt;p&gt;Every error message, every failed installation, every debugging session and every rewritten query contributed to my growth as a Data Analyst.&lt;/p&gt;
&lt;p&gt;The internship has ended—but the learning journey continues.&lt;/p&gt;
&lt;p&gt;Thank you, &lt;strong&gt;Logic Stack&lt;/strong&gt;, for providing a practical environment where I could build real projects, solve real problems and gain valuable industry experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;connect-with-me&quot;&gt;Connect With Me&lt;/h3&gt;
&lt;p&gt;🌐 Portfolio&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;https://yasirawaninfo-data.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💻 GitHub&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831&quot;&gt;https://github.com/YasirAwan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💼 LinkedIn&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/yasirawan4831&quot;&gt;https://linkedin.com/in/yasirawan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for reading. If this journey inspires someone starting their own Data Analytics path, then sharing these experiences has been worthwhile.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;#DataAnalytics #SQL #SQLite #PowerBI #BusinessIntelligence #DataVisualization #Dashboard #GitHub #Internship #LogicStack #DataAnalyst #LearningJourney #OpenToWork #Portfolio #YasirAwan4831&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24267/continuing-my-data-analyst-journey-week-and-challenges-faced-during-data-analyst-intern</guid>
<pubDate>Fri, 07 Aug 2026 19:22:22 +0000</pubDate>
</item>
<item>
<title>Expanding the Sovereign AI Stack: Moving the Specification from Gateway to Local Silicon</title>
<link>https://coderlegion.com/20724/expanding-the-sovereign-ai-stack-moving-the-specification-from-gateway-to-local-silicon</link>
<description>&lt;p&gt;When I first introduced the &lt;a rel=&quot;nofollow&quot; href=&quot;https://kenwalger.github.io/sovereign-system-spec/&quot;&gt;Sovereign Systems Specification&lt;/a&gt; and released the initial foundation of the SDK, &lt;code&gt;sovereign-core&lt;/code&gt; and its accompanying &lt;code&gt;sovereign-fastapi&lt;/code&gt; integration layer (see announcement post &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.kenwalger.com/blog/ai-engineering/sovereign-sdk-release-prose-audit-tax/&quot;&gt;here&lt;/a&gt;), the goal was simple but ambitious: establish a secure, deterministic cryptographic checkpoint at the network ingestion boundary.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sovereign-core&lt;/code&gt; gave local infrastructure a way to anchor identity and validate incoming payloads, while &lt;code&gt;sovereign-fastapi&lt;/code&gt; provided the high-performance middleware necessary to drop those security primitives cleanly into production web runtimes.&lt;/p&gt;
&lt;p&gt;But a secure gateway is only half the battle. As autonomous agents and LLM orchestrators evolve into core enterprise infrastructure, data has to travel deeper into the local topology. It moves across processing loops, through token-minimization filters, and down into persistent storage. If that data isn't armored at every single rest stop, your &quot;sovereign&quot; system still inherits massive operational liabilities.&lt;/p&gt;
&lt;p&gt;To move the ecosystem down the road and secure the entire data lifecycle, I am excited to announce the release of the next two core workspace components of the Sovereign SDK: &lt;code&gt;sovereign-sieve&lt;/code&gt; and &lt;code&gt;sovereign-ledger&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Together, they transition the stack from a server-side perimeter proxy into a complete, end-to-end local data engineering pipeline.&lt;/p&gt;
&lt;h3 id=&quot;1-sovereign-sieve-slicing-the-prose-tax&quot;&gt;1. &lt;code&gt;sovereign-sieve&lt;/code&gt; — Slicing the Prose Tax&lt;/h3&gt;
&lt;p&gt;Before data can be securely audited, it needs to be optimized. Right now, production AI implementations are burning up to 30% of their cloud compute budgets on what I call the &lt;a rel=&quot;nofollow&quot; href=&quot;https://kenwalger.github.io/sovereign-system-spec/terms/prose-tax.html&quot;&gt;Prose Tax&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sovereign-sieve&lt;/code&gt; is an ultra-lightweight, zero-dependency utility that implements our &lt;a rel=&quot;nofollow&quot; href=&quot;https://kenwalger.github.io/sovereign-system-spec/terms/sieve-and-sign-pattern.html&quot;&gt;Sieve-and-Sign Pattern&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead of routing raw conversational noise directly to downstream agents or databases, &lt;code&gt;sovereign-sieve&lt;/code&gt; runs an algorithmic parsing engine locally to clean text streams, isolate underlying data schemas, and strip out fluff. By minimizing your token footprint and context window pressure on local silicon before crossing the ingestion boundary, it turns AI data flow from an unpredictable economic drain into a metered, optimized utility.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Registry:&lt;/strong&gt; &lt;code&gt;pip install sovereign-sieve&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active &amp;amp; Distributed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-sovereign-ledger-the-immutable-data-vault&quot;&gt;2. &lt;code&gt;sovereign-ledger&lt;/code&gt; — The Immutable Data Vault&lt;/h3&gt;
&lt;p&gt;Once data has been sieved by the edge and signed by &lt;code&gt;sovereign-core&lt;/code&gt;, it requires an un-falsifiable record of custody. Standard application logging is notoriously fragile—anyone with &lt;code&gt;root&lt;/code&gt; access or database privileges can alter, backdate, or erase a JSON log file to cover up an algorithmic failure or a security breach.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sovereign-ledger&lt;/code&gt; provides a zero-dependency, append-only, SQLite-backed cryptographic audit store engineered specifically for high-concurrency environments.&lt;/p&gt;
&lt;p&gt;It enforces the specification's &lt;a rel=&quot;nofollow&quot; href=&quot;https://kenwalger.github.io/sovereign-system-spec/terms/write-side-custody.html&quot;&gt;Write-Side Custody&lt;/a&gt; mandate through two tightly integrated layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Engine-Level SQL Triggers:&lt;/strong&gt; Compiled directly inside the database file using &lt;code&gt;BEFORE UPDATE&lt;/code&gt; and &lt;code&gt;BEFORE DELETE&lt;/code&gt; rules that execute a strict &lt;code&gt;RAISE(ROLLBACK, ...)&lt;/code&gt;. Any mutation attempt from &lt;em&gt;any&lt;/em&gt; database client, internal library or external raw connection, is instantly aborted and unwound.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A Linear SHA-256 Hash Chain:&lt;/strong&gt; Every row is mathematically sealed to its predecessor via an eight-column, NUL-delimited (&lt;code&gt;\x00&lt;/code&gt;) canonical preimage. Altering a single timestamp string, tampering with text, or shifting a float precision point out-of-band instantly breaks the chain alignment.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;multi-writer-concurrency-without-mutex-bloat&quot;&gt;Multi-Writer Concurrency Without Mutex Bloat&lt;/h3&gt;
&lt;p&gt;To survive asynchronous ASGI web server runtimes (like FastAPI under Uvicorn), &lt;code&gt;sovereign-ledger&lt;/code&gt; bypasses slow Python-level mutex locks. Instead, it utilizes &lt;code&gt;threading.local()&lt;/code&gt; connection pooling paired with explicit &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; transaction boundaries.&lt;/p&gt;
&lt;p&gt;When multiple concurrent worker threads attempt to write an audit entry, their transactions are cleanly serialized at the SQLite reserved-lock layer, safely queuing inside a 5-second busy_timeout buffer rather than throwing transaction collisions or parent-hash forks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Registry:&lt;/strong&gt; &lt;code&gt;pip install sovereign-ledger&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active &amp;amp; Distributed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-evolving-sovereign-pipeline&quot;&gt;The Evolving Sovereign Pipeline&lt;/h3&gt;
&lt;p&gt;By combining these four pieces, the Sovereign SDK now provides a unified, local-first architecture that handles ingestion, minimization, validation, and storage with zero cloud dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hashlib
from sovereign_sieve import minimize_payload
from sovereign_ledger import SovereignLedger

# 1. Strip the prose tax via sovereign-sieve
clean_text, metrics = minimize_payload(untrusted_user_input)

# 2. Establish identity and state via sovereign-core / gateway logic
mock_receipt = {
    &quot;payload_hash&quot;: hashlib.sha256(clean_text.encode()).hexdigest(),
    &quot;timestamp&quot;: &quot;2026-06-16T10:00:00Z&quot;,
    &quot;signature&quot;: &quot;ecdsa_signature_from_core_gateway&quot;,
    &quot;metadata&quot;: {
        &quot;prose_tax_summary&quot;: metrics
    }
}

# 3. Commit to the immutable vault using sovereign-ledger's context manager
with SovereignLedger(db_path=&quot;.keys/audit_trail.db&quot;) as ledger:
    # Appends atomically and returns the verified payload identifier
    receipt_id = ledger.append_receipt(mock_receipt, clean_text)
    
    # Run a memory-efficient cursor sweep to verify absolute chain integrity
    assert ledger.verify_ledger_integrity(expected_tip_hash=receipt_id) is True
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-s-next-expanding-to-the-edge&quot;&gt;What’s Next: Expanding to the Edge&lt;/h3&gt;
&lt;p&gt;With &lt;code&gt;core&lt;/code&gt;, &lt;code&gt;fastapi&lt;/code&gt;, &lt;code&gt;sieve&lt;/code&gt;, and &lt;code&gt;ledger&lt;/code&gt; stable, the Sovereign Systems Specification has successfully mapped out the gateway and data storage layers. But to truly complete the lineage of local data, we have to go further downstream. All the way to the exact millisecond data is born.&lt;/p&gt;
&lt;p&gt;The next phase of the roadmap will push the boundaries of the SDK out to physical edge silicon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sovereign-sensor&lt;/code&gt;: An ultra-lean cryptographic envelope engine built for MicroPython/CircuitPython (ESP32, Raspberry Pi Pico) to enforce Write-Side Custody at the hardware pin layer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sovereign-edge&lt;/code&gt;: A low-footprint constraint engine optimized for edge compute nodes (Raspberry Pi CM4) to handle structural parsing (§) and offline context snapshots in the field.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core rule remains unyielding: &lt;strong&gt;100% offline silicon execution, zero telemetry leakages, and absolute dependency minimalism&lt;/strong&gt;. Check out the new releases, run the adversarial test suites, and let me know how you’re building local-first governance into your production loops.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kenwalger/sovereign-sdk&quot;&gt;github.com/kenwalger/sovereign-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sovereign Systems Specification: &lt;a rel=&quot;nofollow&quot; href=&quot;https://kenwalger.github.io/sovereign-system-spec/&quot;&gt;https://kenwalger.github.io/sovereign-system-spec/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20724/expanding-the-sovereign-ai-stack-moving-the-specification-from-gateway-to-local-silicon</guid>
<pubDate>Tue, 16 Jun 2026 17:34:04 +0000</pubDate>
</item>
<item>
<title>How SQLite Turns Hardware Chaos into Correctness</title>
<link>https://coderlegion.com/13943/how-sqlite-turns-hardware-chaos-into-correctness</link>
<description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar. I'm building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/HexmosTech/git-lrc&quot;&gt;Star Us&lt;/a&gt; to help devs discover the project. Do give it a try and share your feedback for improving the product.&lt;/em&gt;&lt;/p&gt; 
&lt;p&gt;Databases exist in a hostile world. Disks may fail mid-write. Power cuts interrupt operations. Memory corrupts. Multiple processes access the same file simultaneously. &lt;/p&gt;
&lt;p&gt;Yet somehow, SQLite manages to maintain correctness by ensuring &lt;em&gt;committed transactions persist&lt;/em&gt; and &lt;em&gt;incomplete ones vanish&lt;/em&gt; even when hardware behaves unpredictably.&lt;/p&gt;
&lt;p&gt;This post explores how SQLite transforms unreliable hardware into a reliable database system. &lt;/p&gt;
&lt;p&gt;It is a concise summary of what I’ve learned over the past month while diving into SQLite’s internals, distilled from a longer ongoing series (&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/lovestaco/series/34658&quot;&gt;Understanding SQLite&lt;/a&gt;) and grounded primarily in my study of &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.google.co.in/books/edition/SQLite_Database_System_Design_and_Implem/9Z6IQQnX1JEC?hl=en&amp;amp;gbpv=0&quot;&gt;SQLite Database System: Design and Implementation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We'll trace the journey from physical storage  to the sophisticated mechanisms that guarantee ACID properties, revealing how engineering discipline overcome chaos.&lt;/p&gt;
&lt;h3 id=&quot;the-foundation-understanding-hardware-reality&quot;&gt;The Foundation: Understanding Hardware Reality&lt;/h3&gt;
&lt;p&gt;To appreciate &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sqlite/sqlite&quot;&gt;SQLite&lt;/a&gt;'s correctness guarantees, we must first understand what it's working &lt;em&gt;against&lt;/em&gt;.&lt;/p&gt;
&lt;h4 id=&quot;disks&quot;&gt;Disks&lt;/h4&gt;
&lt;p&gt;At the physical level, persistent storage is fundamentally unreliable.&lt;/p&gt;
&lt;p&gt;Storage devices expose a byte-addressable interface, but provide &lt;strong&gt;no guarantees&lt;/strong&gt; that multi-byte writes are atomic or durable across power failures.&lt;/p&gt;
&lt;p&gt;While filesystems often operate using blocks (commonly 4 KB, but not guaranteed), SQLite cannot rely on any specific block size or atomic-write behavior.&lt;/p&gt;
&lt;p&gt;A write operation can fail midway during a power loss, leaving a page only partially updated.&lt;/p&gt;
&lt;p&gt;This results in &lt;strong&gt;corrupted data&lt;/strong&gt; that may not be detected by the operating system or hardware.&lt;/p&gt;
&lt;p&gt;Consider what happens when a database tries to update data:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/S1aATsm8-l.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;If power fails between steps 3 and 4, the data may be lost.&lt;/p&gt;
&lt;p&gt;If it fails during step 3, the database page may be partially written, corrupting the database.&lt;/p&gt;
&lt;h4 id=&quot;the-non-atomic-write-problem&quot;&gt;The Non-Atomic Write Problem&lt;/h4&gt;
&lt;p&gt;Database systems cannot ignore this reality. &lt;strong&gt;Disk writes are not atomic.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A write operation can fail midway, leaving persistent storage in an inconsistent state. This is why databases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlite.org/wal.html&quot;&gt;write-ahead logging&lt;/a&gt; (WAL)&lt;/li&gt;
&lt;li&gt;Avoid overwriting data blindly&lt;/li&gt;
&lt;li&gt;Rely on journaling, recovery protocols, and defensive I/O ordering&lt;/li&gt;
&lt;li&gt;Treat disks as unreliable, even when hardware claims otherwise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Database designers must explicitly plan for partial writes and silent corruption.&lt;/p&gt;
&lt;h4 id=&quot;concurrency-multiple-processes-one-file&quot;&gt;Concurrency: Multiple Processes, One File&lt;/h4&gt;
&lt;p&gt;SQLite stores an entire database in a &lt;strong&gt;single native file&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This design choice heavily influences how concurrency is implemented.&lt;/p&gt;
&lt;p&gt;Unlike server databases that can use row-level or table-level locking, SQLite coordinates concurrency using a &lt;strong&gt;database-level locking protocol&lt;/strong&gt; built on top of operating-system file-locking primitives.&lt;/p&gt;
&lt;p&gt;Multiple processes may try to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read the database simultaneously&lt;/li&gt;
&lt;li&gt;Write to different tables at the same time&lt;/li&gt;
&lt;li&gt;Modify the same data concurrently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without proper coordination, these operations would corrupt the database file.&lt;/p&gt;
&lt;h3 id=&quot;sqlite-s-defense-strategy-layered-correctness&quot;&gt;SQLite's Defense Strategy: Layered Correctness&lt;/h3&gt;
&lt;p&gt;SQLite doesn't fight hardware chaos with complexity. &lt;/p&gt;
&lt;p&gt;Instead, it uses a carefully designed set of mechanisms that work together to guarantee correctness:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Journals&lt;/strong&gt; for crash recovery&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File locking&lt;/strong&gt; for concurrency control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transaction management&lt;/strong&gt; for atomicity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checksums and validation&lt;/strong&gt; for corruption detection&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's examine each layer.&lt;/p&gt;
&lt;h3 id=&quot;layer-1-journals-the-safety-net&quot;&gt;Layer 1: Journals: The Safety Net&lt;/h3&gt;
&lt;p&gt;SQLite's first line of defense against hardware failures is the &lt;strong&gt;journal&lt;/strong&gt;. &lt;br&gt;
&lt;img src=&quot;https://hackmd.io/_uploads/SkeTI27LWg.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;A journal is a crash recovery file that records database changes so SQLite can roll back incomplete transactions.&lt;/p&gt;
&lt;h4 id=&quot;rollback-journal-before-images-for-recovery&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sqlite.org/tempfiles.html#rollback_journals&quot;&gt;Rollback Journal&lt;/a&gt;: Before Images for Recovery&lt;/h4&gt;
&lt;p&gt;For every database file, SQLite uses a corresponding rollback journal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database: &lt;code&gt;emoji-db-v4.db&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Journal: &lt;code&gt;emoji-db-v4.db-journal&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/B1BDx3XI-l.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;The rollback journal stores &lt;strong&gt;before-images&lt;/strong&gt; of database pages, complete copies of pages as they existed before modification. &lt;/p&gt;
&lt;p&gt;This allows SQLite to restore the database to its previous state if something goes wrong.&lt;/p&gt;
&lt;h5 id=&quot;how-rollback-journals-work&quot;&gt;How Rollback Journals Work&lt;/h5&gt;
&lt;p&gt;When a write transaction begins:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SQLite creates a rollback journal file&lt;/li&gt;
&lt;li&gt;Before modifying any database page, SQLite writes the &lt;strong&gt;entire original page&lt;/strong&gt; to the journal&lt;/li&gt;
&lt;li&gt;SQLite flushes the journal to stable storage&lt;/li&gt;
&lt;li&gt;SQLite modifies the database page in memory&lt;/li&gt;
&lt;li&gt;When the transaction commits, SQLite &lt;strong&gt;finalizes&lt;/strong&gt; the journal (delete, truncate, or zero the header depending on journal mode)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Journal finalization (after database pages are synced) marks the commit point&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If a crash occurs before commit:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The journal file still exists&lt;/li&gt;
&lt;li&gt;On next startup, SQLite detects the journal&lt;/li&gt;
&lt;li&gt;SQLite validates the journal header and checksums&lt;/li&gt;
&lt;li&gt;SQLite reads the before-images from the journal&lt;/li&gt;
&lt;li&gt;SQLite restores all modified pages&lt;/li&gt;
&lt;li&gt;The database returns to its pre-transaction state&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is &lt;strong&gt;physical (age-based) logging&lt;/strong&gt;: every log record stores the entire database page, even if only one byte is modified. &lt;br&gt;
&lt;img src=&quot;https://hackmd.io/_uploads/Sy3aJn7Ibg.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;This approach is intentionally inefficient in space and I/O, but it dramatically simplifies recovery — restoring pages is deterministic and requires no logical replay.&lt;/p&gt;
&lt;h5 id=&quot;journal-structure-segments-and-headers&quot;&gt;Journal Structure: Segments and Headers&lt;/h5&gt;
&lt;p&gt;A rollback journal is divided into &lt;strong&gt;log segments&lt;/strong&gt;. Each segment consists of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;journal header&lt;/strong&gt; (designed to fit within a single disk sector)&lt;/li&gt;
&lt;li&gt;One or more &lt;strong&gt;log records&lt;/strong&gt;, each containing a before-image page&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The header begins with &lt;strong&gt;eight magic bytes&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;D9 D5 05 F9 20 A1 63 D7&lt;/p&gt;
&lt;p&gt;These provide sanity checking and help detect torn or invalid writes.&lt;/p&gt;
&lt;p&gt;The header also stores:&lt;br&gt;
&lt;img src=&quot;https://hackmd.io/_uploads/SkFRghXIWg.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Number of page records (&lt;code&gt;nRec&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Random seed used for checksum calculations&lt;/li&gt;
&lt;li&gt;Original database page count&lt;/li&gt;
&lt;li&gt;Disk sector size&lt;/li&gt;
&lt;li&gt;Database page size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All values are stored in &lt;strong&gt;big-endian format&lt;/strong&gt;, ensuring cross-platform compatibility.&lt;/p&gt;
&lt;h4 id=&quot;statement-journal-fine-grained-rollback&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sqlite.org/tempfiles.html#statement_journal_files&quot;&gt;Statement Journal&lt;/a&gt;: Fine-Grained Rollback&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;statement journal&lt;/strong&gt; exists for one specific purpose: undoing the partial effects of a &lt;em&gt;single SQL statement&lt;/em&gt; that fails midway, without aborting the surrounding transaction.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;An &lt;code&gt;INSERT&lt;/code&gt; statement attempts to insert 1000 rows. If a constraint violation occurs at row 500, the first 499 inserts must be undone, but the user transaction must remain active.&lt;/p&gt;
&lt;p&gt;The statement journal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is a &lt;strong&gt;separate rollback journal file&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stored as a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sqlite.org/tempfiles.html&quot;&gt;temporary file&lt;/a&gt; (random &lt;code&gt;etilqs_*&lt;/code&gt; name)&lt;/li&gt;
&lt;li&gt;Lives in the system temp directory&lt;/li&gt;
&lt;li&gt;Exists &lt;strong&gt;only during statement execution&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Is deleted immediately after the statement completes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/By0mZ3mL-e.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;Like rollback journals, statement journals &lt;strong&gt;store before-image pages on disk&lt;/strong&gt;.&lt;br&gt;
However:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They are &lt;strong&gt;not used for crash recovery&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;They are ignored during database startup&lt;/li&gt;
&lt;li&gt;They exist only to support statement-level atomicity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQLite tracks statement journal metadata in memory, allowing it to roll back a single statement without aborting the user transaction.&lt;/p&gt;
&lt;h4 id=&quot;master-journal-multi-database-atomicity&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/lovestaco/sqlite-source-code/blob/master/ext/misc/vfsstat.c&quot;&gt;Master Journal&lt;/a&gt;: Multi-Database Atomicity&lt;/h4&gt;
&lt;p&gt;SQLite allows multiple databases to be attached to a single library connection:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/ryoPM3mIZg.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;A single transaction may modify multiple databases. Each database has its own rollback journal, but atomicity must be preserved across &lt;em&gt;all&lt;/em&gt; of them.&lt;/p&gt;
&lt;p&gt;To achieve this, SQLite introduces the &lt;strong&gt;master journal&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The master journal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is created &lt;strong&gt;during commit preparation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Exists only for multi-database write transactions&lt;/li&gt;
&lt;li&gt;Is deleted once commit completes&lt;/li&gt;
&lt;li&gt;Contains &lt;strong&gt;no page images&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stores the &lt;strong&gt;names of participating rollback journals&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/S1Vxz3Q8Zg.png&quot; alt=&quot;image&quot;&gt;&lt;br&gt;
Each participating rollback journal becomes a &lt;strong&gt;child journal&lt;/strong&gt;. During commit, each child journal appends a &lt;strong&gt;master journal record&lt;/strong&gt; containing.&lt;/p&gt;
&lt;p&gt;If SQLite crashes during commit:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Recovery examines child journals&lt;/li&gt;
&lt;li&gt;Child journals reference the master journal&lt;/li&gt;
&lt;li&gt;The master journal confirms which journals belong together&lt;/li&gt;
&lt;li&gt;Either all databases are rolled back, or all are committed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This guarantees &lt;strong&gt;global atomicity across attached databases&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;layer-2-file-locking&quot;&gt;Layer 2: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlite.org/lockingv3.html&quot;&gt;File Locking&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Journals protect against crashes, but they don't solve concurrency. &lt;/p&gt;
&lt;p&gt; SQLite relies on &lt;strong&gt;file locking&lt;/strong&gt; to coordinate access across processes.&lt;/p&gt;
&lt;h4 id=&quot;db-level-locking-model-rollback-journal-mode-&quot;&gt;DB Level Locking Model (Rollback-Journal Mode)&lt;/h4&gt;
&lt;p&gt;In rollback-journal mode, SQLite enforces strict rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple readers may read concurrently&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;one writer&lt;/strong&gt; may exist at a time&lt;/li&gt;
&lt;li&gt;Readers block writers&lt;/li&gt;
&lt;li&gt;Writers block both readers and other writers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This coarse-grained locking model simplifies correctness and eliminates deadlocks, at the cost of reduced concurrency.&lt;/p&gt;
&lt;h4 id=&quot;lock-states-and-transitions&quot;&gt;Lock States and Transitions&lt;/h4&gt;
&lt;p&gt;SQLite uses five lock states:&lt;br&gt;
&lt;img src=&quot;https://hackmd.io/_uploads/BkRgQhX8Zx.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UNLOCKED&lt;/strong&gt;: No locks held&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SHARED&lt;/strong&gt;: Multiple readers allowed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RESERVED&lt;/strong&gt;: Writer intends to write&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PENDING&lt;/strong&gt;: Writer waiting for readers to finish&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EXCLUSIVE&lt;/strong&gt;: Writer actively modifying db&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Typical transitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Readers acquire &lt;strong&gt;SHARED&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Writers acquire &lt;strong&gt;SHARED → RESERVED → PENDING → EXCLUSIVE&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Write locks are released at transaction end&lt;/li&gt;
&lt;li&gt;Read locks may be released earlier, depending on execution flow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hackmd.io/_uploads/r1p4QnXIWx.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;continue-reading-the-rest-of-article&quot;&gt;Continue reading the rest of article&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://journal.hexmos.com/understanding-sqlite/#layer-3-transaction-managementhttpswwwsqliteorglang-transactionhtml&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My experiments and hands-on executions related to SQLite will live here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/lovestaco/sqlite/tree/master/sqlite-examples&quot;&gt;lovestaco/sqlite&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;references-&quot;&gt;References:&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.google.co.in/books/edition/SQLite_Database_System_Design_and_Implem/9Z6IQQnX1JEC?hl=en&amp;amp;gbpv=0&quot;&gt;SQLite Database System: Design and Implementation. N.p.: Sibsankar Haldar, (n.d.).&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13943/how-sqlite-turns-hardware-chaos-into-correctness</guid>
<pubDate>Sat, 04 Apr 2026 10:30:57 +0000</pubDate>
</item>
<item>
<title>SQLite - High-Level Introduction</title>
<link>https://coderlegion.com/3458/sqlite-high-level-introduction</link>
<description>&lt;h2 id=&quot;sqlite&quot;&gt;SQLite&lt;/h2&gt;
&lt;p&gt;SQLite is a self-contained, serverless, zero-configuration relational database engine. Unlike traditional database servers, SQLite is distributed as a simple library that you embed directly into your applications. This guide will introduce you to SQLite's core concepts, explain why it's such a popular choice for developers, highlight how it differs from other database systems, and show how you can leverage it inside Divooka's visual programming environment.&lt;/p&gt;
&lt;h3 id=&quot;why-sqlite-&quot;&gt;Why SQLite?&lt;/h3&gt;
&lt;p&gt;For many new programmers, setting up a client–server database (e.g., MySQL, PostgreSQL) can feel like learning two things at once: programming &lt;strong&gt;and&lt;/strong&gt; system administration. SQLite removes that overhead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero-configuration&lt;/strong&gt;&lt;br&gt;
No daemon to install or manage - your application just opens a file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightweight and portable&lt;/strong&gt;&lt;br&gt;
Entire database lives in a single cross-platform file (often just a few hundred kilobytes of library code).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broad language support&lt;/strong&gt;&lt;br&gt;
Bindings exist for Python, JavaScript, C#, Java, Go, Rust, and more - so you can start querying data in the language you already know.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of its simplicity, SQLite is the most widely deployed database engine in the world, powering everything from mobile apps and desktop software to IoT devices and web browsers.&lt;/p&gt;
&lt;h3 id=&quot;core-architecture&quot;&gt;Core Architecture&lt;/h3&gt;
&lt;h4 id=&quot;file-based-storage&quot;&gt;File-Based Storage&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single file&lt;/strong&gt;: Definitions, tables, indexes, and data all live in one &lt;code&gt;.db&lt;/code&gt; (or &lt;code&gt;.sqlite3&lt;/code&gt;) file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform&lt;/strong&gt;: You can copy the database file between Windows, macOS, Linux, and mobile devices without conversion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The file-based nature of SQLite makes it super easy to backup and migrate databases.&lt;/p&gt;
&lt;h4 id=&quot;serverless-and-embedded&quot;&gt;Serverless and Embedded&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Library, not a server&lt;/strong&gt;: Applications link against &lt;code&gt;libsqlite3&lt;/code&gt; and call C-APIs (or higher-level wrappers) directly - there's no separate &quot;database process.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced latency&lt;/strong&gt;: Reads and writes happen in-process, avoiding inter-process communication overhead found in client–server setups.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;dynamic-typing-manifest-typing-&quot;&gt;Dynamic Typing (&quot;Manifest Typing&quot;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Types on values, not columns&lt;/strong&gt;: Columns have &quot;type affinities&quot; (e.g., &lt;code&gt;INTEGER&lt;/code&gt;, &lt;code&gt;TEXT&lt;/code&gt;), but SQLite won't reject a string inserted into an &lt;code&gt;INTEGER&lt;/code&gt; column - it will try to convert it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional strict mode&lt;/strong&gt;: As of recent versions, you can declare &lt;code&gt;CREATE TABLE ... STRICT&lt;/code&gt; to enforce traditional SQL type checking if you need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;concurrency-model&quot;&gt;Concurrency Model&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reader–writer locks&lt;/strong&gt;: Multiple readers can operate simultaneously, but writes acquire an exclusive lock on the file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Ahead Logging (WAL)&lt;/strong&gt;: Enables concurrent reads and writes by appending changes to a separate log, improving throughput for mixed workloads.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;key-differences-from-other-databases&quot;&gt;Key Differences from Other Databases&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Aspect               &lt;/th&gt;&lt;th&gt;SQLite                                  &lt;/th&gt;&lt;th&gt;Client–Server Databases&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Deployment           &lt;/td&gt;&lt;td&gt;Embed a library; open a local file      &lt;/td&gt;&lt;td&gt;Install/maintain a separate server&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Configuration        &lt;/td&gt;&lt;td&gt;Zero-config; managed by file system     &lt;/td&gt;&lt;td&gt;Requires user accounts, ports, etc.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Process architecture &lt;/td&gt;&lt;td&gt;In-process library calls                &lt;/td&gt;&lt;td&gt;Inter-process with network protocol&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Typing               &lt;/td&gt;&lt;td&gt;Dynamic, manifest typing                &lt;/td&gt;&lt;td&gt;Static, enforced column types&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Concurrency          &lt;/td&gt;&lt;td&gt;Serialized writes (with WAL option)     &lt;/td&gt;&lt;td&gt;True concurrent writes via server&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Backup               &lt;/td&gt;&lt;td&gt;Copy the file or use online backup APIs &lt;/td&gt;&lt;td&gt;Use dump utilities or replication&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;practical-use-cases&quot;&gt;Practical Use Cases&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Desktop and Mobile Apps&lt;/strong&gt;&lt;br&gt;
Store user settings, preferences, and offline data without bundling a full database server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded and IoT Devices&lt;/strong&gt;&lt;br&gt;
Firmware and edge-computing modules often rely on SQLite for local data persistence, thanks to its tiny footprint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prototyping and Testing&lt;/strong&gt;&lt;br&gt;
Quickly spin up a relational store for experiments and unit tests - no setup scripts needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High-Availability Reads&lt;/strong&gt;&lt;br&gt;
Applications that require many concurrent read operations (e.g., analytics dashboards) benefit from SQLite's low-latency access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File-Based Data Interchange&lt;/strong&gt;&lt;br&gt;
Sharing a &lt;code&gt;.db&lt;/code&gt; file between teams or tools (e.g., GIS software, data science notebooks) is as simple as sharing a CSV.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;sqlite-in-divooka&quot;&gt;SQLite in Divooka&lt;/h3&gt;
&lt;p&gt;In Divooka's visual programming language, SQLite plays two roles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;In-Memory Data Engine&lt;/strong&gt;: Run queries against data you're manipulating on the canvas - think temporary tables without disk I/O.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent Storage&lt;/strong&gt;: Divooka Explore ships with SQLite pre-installed, so you can save and load your data without any extra configuration - just specifying a filepath for the database and you are good to go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple Web Interface&lt;/strong&gt;: The O'ista web framework offers a capability for you to host SQLite online with authentication, so the database can be accessed by others.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This integration means you can drag a &quot;SQL Query&quot; node onto your workflow, point it at either an in-memory dataset or a saved &lt;code&gt;.db&lt;/code&gt; file, and immediately start filtering, aggregating, and joining tables - all within Divooka's visual interface.&lt;/p&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open/Create a database&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- In the sqlite3 shell:
.open my_project.db
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create tables&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  joined DATE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Insert sample data&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO users (name, joined)
VALUES ('Alice', '2025-06-01'),
       ('Bob',   '2025-06-15');
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run a query&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, name
FROM users
WHERE joined &amp;gt;= '2025-06-10';
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In Divooka Explore, these same SQL snippets can be entered into a &lt;strong&gt;SQL&lt;/strong&gt; query block node, letting you mix SQL with visual data flows.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;SQLite offers a frictionless path for beginners to learn SQL and build data-driven applications, while still providing advanced features like ACID transactions, full-text search, and JSON support for more experienced developers. Its unique combination of simplicity, portability, and performance makes it an ideal companion for both standalone projects and integrations inside environments like Divooka. Whether you're just exploring databases for the first time or architecting an embedded system, SQLite is a tool that scales with your needs - no server required.&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;See also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL refresher on Methodox Wiki  &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.methodox.io/KnowledgeBase/SQL&quot;&gt;Knowledge Base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;O'ista web framework &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.methodox.io//Frameworks/Oista/Capabilities&quot;&gt;capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SQLite official &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlite.org/faq.html&quot;&gt;FAQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3458/sqlite-high-level-introduction</guid>
<pubDate>Tue, 24 Jun 2025 15:27:25 +0000</pubDate>
</item>
<item>
<title>Database Interaction using Python: Introduction to SQLite/MySQL/PostgreSQL</title>
<link>https://coderlegion.com/1586/database-interaction-using-python-introduction-to-sqlite-mysql-postgresql</link>
<description>&lt;p&gt;The article covers POSTGRESQL, MySQL, and SQLITE3—three major database management systems. They provide unique features and benefits and are essential to software development. Let's explore their features so that you can make better selections that will improve the project. Together, let's explore these essential elements in modern software engineering.&lt;/p&gt;
&lt;p&gt;As SQLite is &lt;strong&gt;lightweight&lt;/strong&gt;, it is a great choice for embedded applications and situations where simplicity is essential. On the other hand, MySQL stands out thanks to its strong&lt;strong&gt; client-server design&lt;/strong&gt;, &lt;strong&gt;scalability&lt;/strong&gt;, and widespread industry acceptance. PostgreSQL, on the other hand, is a popular option for&lt;strong&gt; enterprise-level &lt;/strong&gt;applications due to its &lt;strong&gt;versatility&lt;/strong&gt;, &lt;strong&gt;dependability&lt;/strong&gt;, and compliance with ACID short for Atomicity, Consistency, Isolation, and Durability.&lt;/p&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;SQLite Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;SQLite is a relational database engine that is lightweight, serverless, self-contained, and requires no configuration. Because it works directly with files, it's perfect for low-resource applications. Distribution and deployment are made easier by the fact that its self-contained databases are kept as single files. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedded in applications&lt;/strong&gt;: SQLite, which is frequently integrated into programmes, allows internal data administration without the need for external servers. Data management is made easier by this, especially for offline functioning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File-based database&lt;/strong&gt;: SQLite databases are manageable and portable since they are kept as individual files on the file system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Appropriate for small to medium-sized applications&lt;/strong&gt;: Because of its simplicity and lightweight design, SQLite is especially well-suited for small to medium-sized applications, even though it can manage huge datasets as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's go through an example of defining tables, installing SQLite in Python, and carrying out CRUD (Create, Read, Update, Delete) operations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;conn = sqlite3.connect('example.db')&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users&lt;br&gt;
                  (id INTEGER PRIMARY KEY, name TEXT, email TEXT)''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;, ('Ali', '*Emails are not allowed*'))&lt;br&gt;
print(&quot;Records after initial insertion:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;br&gt;
cursor.execute(&quot;UPDATE users SET email = ? WHERE name = ?&quot;, ('*Emails are not allowed*', 'Alice'))&lt;br&gt;
print(&quot;\nRecords after updating email of Alice:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;br&gt;
cursor.execute(&quot;DELETE FROM users WHERE name = ?&quot;, ('Alice',))&lt;br&gt;
print(&quot;\nRecords after deleting Alice:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=8245787205156792733&quot; alt=&quot;&quot; width=&quot;600&quot; height=&quot;250&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt; Make sure the &lt;code&gt;'DISTINCT'&lt;/code&gt; keyword is applied to the right columns in SQL queries to obtain unique value combinations and prevent accidental duplicate results.&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;MySQL Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The popular open-source RDBMS MySQL provides strong features and dependability for systems ranging from small-scale initiatives to enterprise-level setups. It offers a wide range of tools and features for effective structured data management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client-server architecture:&lt;/strong&gt;MySQL has a client-server design, which enables several clients to connect to the database server at once. The management of database resources, scalability, and security are all improved by this architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance and scalability:&lt;/strong&gt; MySQL is designed to be scalable, meaning it can manage big databases and high transaction volumes while maintaining optimal performance. Performance is improved by its effective caching, query optimisation, and indexing methods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broad industrial adoption:&lt;/strong&gt; MySQL is widely used in a number of industries, including telecommunications, e-commerce, technology, and finance. Its widespread community support, dependability, and open-source nature have all added to its appeal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can get the MySQL Community Server package from &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.mysql.com/downloads/mysql/&quot;&gt; mysql official website&lt;/a&gt; and install MySQL by following the given steps. As an alternative, you can use package managers such as Homebrew for macOS or apt-get for Linux.&lt;/p&gt;
&lt;p&gt;Connecting to MySQL from Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import mysql.connector&lt;br&gt;
conn = mysql.connector.connect(user='user', password='password',&lt;br&gt;
                               host='localhost',&lt;br&gt;
                               database='test')&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute(&quot;SELECT * FROM table_name&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
conn.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating tables and executing queries using Python's MySQL Connector:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users&lt;br&gt;
                  (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (%s, %s)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;SELECT * FROM users&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=4398450430378255567&quot; alt=&quot;&quot; width=&quot;650&quot; height=&quot;400&quot;&gt;&lt;/p&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;PostgreSQL Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Strong and open-source, PostgreSQL is an &lt;strong&gt;object-relational database system&lt;/strong&gt; that is well-known for its feature-rich set, dependability, and extensibility. It is a top option for both small-scale applications and large corporate systems because of its comprehensive functions that enable complicated data types, transactions, and data integrity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Extensibility&lt;/strong&gt;: Because of PostgreSQL's flexible architecture, programmers are able to design unique functions, data types, and indexing techniques to meet the needs of particular applications. The smooth integration of third-party extensions and libraries is made possible by this flexibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACID Compliance&lt;/strong&gt;: PostgreSQL guarantees ACID (&lt;br&gt;&lt;strong&gt;Atomicity&lt;/strong&gt;: To preserve database integrity, transactions are either fully completed or entirely aborted.&lt;br&gt;&lt;strong&gt;Consistency&lt;/strong&gt;: Assures that the database is kept intact by writing only legitimate data.&lt;br&gt;&lt;strong&gt;Isolation&lt;/strong&gt;: Data integrity is ensured by many transactions running simultaneously without interfering.&lt;br&gt;&lt;strong&gt;Durability&lt;/strong&gt;: Even in the event of a system failure, committed transaction modifications remain intact.)  compliance, which in turn provides dependable and consistent database transactions. This functionality is crucial for ensuring consistency across the database and data integrity even in the case of system failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Integrity and Reliability&lt;/strong&gt;: PostgreSQL uses built-in restrictions like triggers and rules, foreign keys, and concurrency management to guarantee data integrity and stability.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;p&gt;To install PostgreSQL, you can download and install the PostgreSQL package from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/download/&quot;&gt; official website&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
conn = psycopg2.connect(&lt;br&gt;
    dbname='your_database_name',&lt;br&gt;
    user='your_username',&lt;br&gt;
    password='your_password',&lt;br&gt;
    host='localhost',&lt;br&gt;
    port='5432'&lt;br&gt;
)&lt;br&gt;
if conn:&lt;br&gt;
    print(&quot;Connected to PostgreSQL&quot;)&lt;br&gt;
else:&lt;br&gt;
    print(&quot;Failed to connect to PostgreSQL&quot;)&lt;br&gt;
conn.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating tables and executing queries using Python's psycopg2 library:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;# Create a cursor object&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users (&lt;br&gt;
                    id SERIAL PRIMARY KEY,&lt;br&gt;
                    name VARCHAR(255),&lt;br&gt;
                    email VARCHAR(255)&lt;br&gt;
                )''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (%s, %s)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;SELECT * FROM users&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
for row in results:&lt;br&gt;
    print(row)&lt;br&gt;
conn.commit()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-blue&quot;&gt;&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt; Always securely handle database credentials and avoid hardcoding them directly into your code for better security.&lt;/div&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Comparison&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;table border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Good for &lt;br&gt;small to medium&lt;br&gt;-sized applications&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;Optimized for &lt;br&gt;read-heavy &lt;br&gt;workloads&lt;/td&gt;
&lt;td&gt;Optimized for &lt;br&gt;complex &lt;br&gt;queries &lt;br&gt;and large &lt;br&gt;datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Limited scalability &lt;br&gt;due to file-&lt;br&gt;based architecture&lt;/td&gt;
&lt;td&gt;Good scalability &lt;br&gt;with support for &lt;br&gt;clustering and &lt;br&gt;sharding&lt;/td&gt;
&lt;td&gt;High scalability &lt;br&gt;with advanced &lt;br&gt;replication and &lt;br&gt;partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;Basic features with limited concurrency support&lt;/td&gt;
&lt;td&gt;Rich feature set including stored procedures, triggers, and views&lt;/td&gt;
&lt;td&gt;Comprehensive feature set including JSONB data type, &lt;br&gt;full-text search, and &lt;br&gt;advanced indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ease of Use&lt;/td&gt;
&lt;td&gt;Simple setup and usage, suitable for beginners&lt;/td&gt;
&lt;td&gt;Easy to use, especially &lt;br&gt;with graphical tools like MySQL Workbench&lt;/td&gt;
&lt;td&gt;Requires more &lt;br&gt;configuration but &lt;br&gt;offers powerful features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use Case Suitability&lt;/td&gt;
&lt;td&gt;Ideal for &lt;br&gt;embedded &lt;br&gt;systems &lt;br&gt;and small &lt;br&gt;applications&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Well-suited for web applications &lt;br&gt;and medium-&lt;br&gt;sized databases&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;Best for large-scale applications with &lt;br&gt;complex data &lt;br&gt;requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Advanced Concepts&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Advanced ideas like indexing, optimization, and transactions are essential for improving the scalability, performance, and dependability of databases.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transactions:&lt;/strong&gt; Transactions ensure the ACID properties of database operations, handling multiple tasks as a single unit for data accuracy and reliability. All database systems provide tools for effective transaction management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Indexing&lt;/strong&gt;: Indexes expedite data retrieval, particularly for SELECT queries, by swiftly accessing entries in database tables based on specific field values. Effective indexing reduces the need for full table scans, significantly enhancing query performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimization&lt;/strong&gt;: Database optimization strategies improve effectiveness and performance through resource usage optimization, performance tuning, query optimization, and schema design optimization. Each system provides unique features and tools to enhance productivity and address performance issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import sqlite3&lt;br&gt;
import mysql.connector&lt;br&gt;
import psycopg2&lt;br&gt;
def sqlite_transactions():&lt;br&gt;
    conn = sqlite3.connect('example.db')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.execute(&quot;BEGIN&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except sqlite3.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
def mysql_transactions():&lt;br&gt;
    conn = mysql.connector.connect(user='user', password='password', database='example')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.start_transaction()&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except mysql.connector.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
def postgres_transactions():&lt;br&gt;
    conn = psycopg2.connect(dbname='example', user='user', password='password', host='localhost')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.autocommit = False&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except psycopg2.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
sqlite_transactions()&lt;br&gt;
mysql_transactions()&lt;br&gt;
postgres_transactions()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SQLite, designed for single-user and small-scale applications, uses local file-based databases and simple transaction management with BEGIN, COMMIT, and ROLLBACK statements. &lt;br&gt;MySQL, a robust RDBMS, offers scalability and concurrency control with start_transaction(), commit(), and rollback() methods.&lt;br&gt;PostgreSQL, known for advanced features and extensibility, provides strong support for concurrency, data integrity, and advanced SQL features, managing transactions via autocommit mode and commit()/rollback() methods.&lt;/p&gt;
&lt;/section&gt;
&lt;div class=&quot;div-faq&quot;&gt;&lt;span class=&quot;faq-header&quot;&gt;FAQ&lt;/span&gt; &lt;span class=&quot;faq-body&quot;&gt;&lt;br&gt;Q: What is the difference between SQL and NoSQL databases?&lt;br&gt;A: SQL (Structured Query Language) databases, also known as relational databases, organize data into tables with predefined schemas and use SQL for querying. NoSQL databases, on the other hand, store data in flexible, non-tabular formats and do not require a fixed schema. They are designed for scalability and handling unstructured data.&lt;/span&gt;&lt;/div&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Databases are essential to software development because they allow for effective data management. MySQL scales for the web, PostgreSQL meets enterprise needs, and SQLite is ideal for&lt;strong&gt; lightweight applications&lt;/strong&gt;. When choosing a project, it is essential to comprehend their strengths. Using &lt;strong&gt;cutting-edge ideas&lt;/strong&gt; promotes creativity in digital solutions by ensuring data performance and dependability.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;For more reference:&lt;br&gt;SQLite Official Website:&lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite Official Website&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
</description>
<category>Python Database Interaction</category>
<guid isPermaLink="true">https://coderlegion.com/1586/database-interaction-using-python-introduction-to-sqlite-mysql-postgresql</guid>
<pubDate>Wed, 19 Mar 2025 11:56:30 +0000</pubDate>
</item>
</channel>
</rss>