Skip to content

code-by-sia/xi-sqlite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

xi-sqlite

SQLite bindings for Xi apps — pure Xi, no glue C.

The library binds the system sqlite3 directly through Xi's extern "C" FFI (link "sqlite3") and exposes everything through interfaces resolved by Xi's DI container: depend on sqlite.SQLite (connections and queries), sqlite.RowReader (typed access to result rows), or sqlite.ColumnDecoder (how columns become values), and the bundled implementations are injected automatically — or rebind any of them in your module App / module Test. Every fallible call returns T!, never aborts.

Install

Copy or vendor the src/ contents — sqlite.xi plus the sqlite/ folder — into your project and import the umbrella file (always that one; it loads the parts in the required order):

import "vendor/sqlite.xi"     // wherever you placed it

Requires the SQLite library on the host (preinstalled on macOS; libsqlite3-dev on Debian/Ubuntu).

Usage

Keep SQL out of your entry: put it in a store class behind a repository interface, and let the entry depend on the abstraction only. examples/demo.xi is the full version of this sketch:

import "std/log.xi"
import "vendor/sqlite.xi"

type Note        = { id: Integer, title: String, stars: Number }
type Notes       = { items: List<Note> }
type NoteSession = { db: sqlite.Database }

interface NoteRepository {
    producer open(path: String) -> NoteSession!
    producer add(s: NoteSession, title: String, stars: Number) -> Integer!
    producer list(s: NoteSession) -> Notes!
    producer close(s: NoteSession) -> Bool!
}

// all SQL lives here; the rest of the app never sees it
class NoteSQLiteStore implements NoteRepository {
    deps { sql: sqlite.SQLite, reader: sqlite.RowReader }

    producer open(path: String) -> NoteSession! {
        let db = sql.open(path)?
        let made = sql.exec(db, "create table if not exists notes (id integer primary key, title text not null, stars real not null)")
        if isErr(made) { return err(made.err) }
        return ok(NoteSession { db: db })
    }

    producer list(s: NoteSession) -> Notes! {
        let rows = sql.query(s.db, "select id, title, stars from notes order by id")
        if isErr(rows) { return err(rows.err) }
        let notes = empty List<Note>
        for row in rows.value.items {
            notes.push(Note {
                id:    reader.intAt(row, "id", 0),
                title: reader.textAt(row, "title", ""),
                stars: reader.numberAt(row, "stars", 0.0)
            })
        }
        return ok(Notes { items: notes })
    }
    // ... add/close elided
}

async entry (logger: Logger, notes: NoteRepository) main(args: String[]) -> Integer {
    let opened = notes.open("app.db")
    if isErr(opened) { logger.error(opened.err) return 1 }
    let session = opened.value

    let added = notes.add(session, "hi", 5.0)
    if isErr(added) { logger.error(added.err) return 1 }

    let all = notes.list(session)
    if isErr(all) { logger.error(all.err) return 1 }
    for n in all.value.items { logger.info("#" + n.id + " " + n.title) }

    let closed = notes.close(session)
    if isErr(closed) { logger.error(closed.err) return 1 }
    return 0
}

module App {}

Overriding: every seam is an interface, so swapping behavior is one bind — fake the whole database in tests (module Test { bind sqlite.SQLite -> FakeSQLite }), swap persistence (bind NoteRepository -> InMemoryNotes), or change column decoding for every query (bind sqlite.ColumnDecoder -> MyDecoder). Tests can also inject the real thing per test block: test "queries" (sql: sqlite.SQLite, reader: sqlite.RowReader) { ... }, as tests/sqlite_test.xi does.

API

interface sqlite.SQLite — connections and queries (impl: SystemSQLite)

Method Returns Notes
open(path) Database! creates the file if missing; ":memory:" for in-memory
close(db) Bool! errs if statements are still open
exec(db, sql) Bool! one or more ;-separated statements, no result rows
query(db, sql) Rows! typed rows; iterate rows.items
queryJson(db, sql) String! rows as a JSON array of objects
lastInsertId(db) Integer rowid of the last insert
changes(db) Integer rows affected by the last statement

interface sqlite.RowReader — typed row access (impl: TypedRowReader)

Each Row maps column names to typed Values (IntValue, RealValue, TextValue, NullValue). Missing columns and type mismatches yield the fallback you pass:

reader.intAt(row, "id", 0)          // Integer
reader.numberAt(row, "price", 0.0)  // Number (integers widen)
reader.textAt(row, "title", "")     // String
reader.isNull(row, "deleted_at")    // Bool
reader.hasColumn(row, "id")         // Bool

interface sqlite.ColumnDecoder — column → Value → Json (impl: TypedColumnDecoder)

SystemSQLite runs every result column through this; rebind it to change how all queries decode. BLOB columns are surfaced as text (sqlite's cast of the raw bytes); select hex(col) in SQL when you need a stable binary encoding.

Layout

  • src/sqlite.xi — umbrella file; the one apps import
  • src/sqlite/api.xinamespace sqlite: the data types (Database, Value, Row, Rows), the interfaces (SQLite, RowReader, ColumnDecoder), and the Value-handling defaults (TypedRowReader, TypedColumnDecoder)
  • src/sqlite/system.xiSystemSQLite, the stock implementation over libsqlite3
  • src/sqlite/ffi.xi — raw extern "C" binding (internal)
  • examples/demo.xi — layered demo app: ./scripts/run-demo.sh
  • tests/sqlite_test.xi — test suite: ./scripts/run-tests.sh

Types, interfaces, and the Value-matching classes share api.xi because a Xi namespace cannot span files; the parts deliberately don't import each other (Xi resolves imports by literal path and would load a file reached via two spellings twice), so always import the umbrella sqlite.xi.

Develop

./scripts/run-demo.sh     # builds with xc, runs build/sqlite-demo
./scripts/run-tests.sh    # xi test tests/sqlite_test.xi

Expected demo output:

[info] inserted up to id 2
[info] #1 hello from Xi (4.5 stars)
[info] #2 no C bridge needed (5 stars)

About

SQLite library port for Xi language

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors