The Lighter Swift Codegen for SQLite3

Lighter is a set of technologies applying code generation to access SQLite3 databases from Swift, e.g. in iOS applications or on the server. Like SwiftGen but for SQLite3.

Type-safe down to the SQL schema. State of the art: Developer writes Swift structures that match a SQLite table. Enlighter reverses this, the generated Swift code reflects what the SQLite table is actually defined as. No place for mistakes. Remove a “somewhere” in the “it’s always any-o-clock somewhere in the stack”.
Very, very, fast. Lighter builds upon the database schema and hence directly knows what it looks like at compile time. For common operations no mapping is necessary at all, the generated code runs as fast (usually faster) than hand written code. It directly binds Swift structures to the SQLite API.
Dependency free. Lighter itself is a small and convenient API to access SQLite databases. Enlighter, the code generator, can however produce code that just uses the SQLite API and doesn’t require any dependencies, i.e. tech debt. Don’t ship 3rd party libraries, directly generate the necessary code into your app.

Lighter is useful for two main scenarios:

Shipping SQLite databases within your app (e.g. containing a product database). SQLite databases are very resource efficient way to ship and access small and big amounts of data. As an alternative to bundling JSON resources files that are large and have to be parsed fully into memory on each start. With a SQLite database only the required data needs to be loaded and the database files are extremely compact (e.g. no duplicate keys). > SQLite database are also efficient and useful for downloading > data from the network!
Maintaining a fast local SQL cache or database. If the needs are simpler than a full [ORM](https://en.wikipedia.org/wiki/Object–relational_mapping) like [CoreData](https://developer.apple.com/documentation/coredata), Lighter can be a great way to produce neat and typesafe APIs for local caches or databases. It is basic but convenient to use and very very fast as no runtime mapping or parsing has to happen at all. The code directly binds the generated structures to the SQLite API. Databases can be created on the fly or from prefilled database files shipped as part of the application resources. > Linux is also supported, and Lighter can be a great choice for simple servers that > primarily access a readonly set or run on a single host.

Overview

Lighter works the reverse from other “mapping” tools or SQLite wrappers. Instead of writing Swift code that generates SQLite tables dynamically, Lighter generates Swift code for a SQLite database. Either literally from SQLite database files, or from SQL files that create SQLite databases.

Small Example Database (stored in either a SQLite db or created from .sql files):

CREATE TABLE person (
  person_id INTEGER PRIMARY KEY NOT NULL,
  name      TEXT NOT NULL,
  title     TEXT NULL
);

CREATE TABLE address (
  address_id INTEGER PRIMARY KEY NOT NULL,
  
  street  VARCHAR NULL,
  city    VARCHAR NULL,
  
  person_id INTEGER,
  FOREIGN KEY(person_id) REFERENCES person(person_id) ON DELETE CASCADE DEFERRABLE
);

Can be converted to a structure like this (in a highly configurable way):

struct ContactsDB {

  struct Person: Identifiable, Hashable {
    var id       : Int
    var name     : String
    var title    : String?
  }

  struct Address: Identifiable, Hashable {
    var id       : Int
    var street   : String?
    var city     : String?
    var personId : Int?
  }
}

The code generator can either generate dependency free code that only uses the raw SQLite3 API or code that uses the Lighter library. The Lighter library is not an ORM, but just a set of Swift protocols that allow for typesafe queries (and it is only intended to be used to support the code generator, not as a standalone library).

How does the code generation work?

The setup is intended to work with the new Swift Package Plugins feature of the Swift Package Manager, available since Swift 5.6 (and exposed in Xcode 14+). If SPM plugins cannot be used yet, the sqlite2swift tool can be called directly as well.
If you want to support the project, there is also the Code for SQLite3 app on the Mac AppStore. It does the same code generation as this FOSS project in a little more interactive way.

The Lighter package comes with a “build tool plugin” called Enlighter, that automatically integrates the code generation results into the build process. If it is added to a target, it’ll scan for databases and SQL files and create the Swift accessors for them:

.target(name: "ContactsDB", dependencies: [ "Lighter" ],
        resources: [ .copy("ContactsDB.sqlite3") ],
        plugins: [ "Enlighter" ]) // <== tell SPM to use Enlighter on this target

This variant is fully automatic, i.e. other code within the ContactsDB target has direct access to the database types (e.g. the Person struct above).

As a manual alternative the Generate Code for SQLite “command plugin” is provided. This plugin does the same generation as Enlighter, but is explicitly run by the developer using the Xcode “File / Packages” menu. It places the resulting code into the “Sources” folder of the app (where it can be inspected or modified).

Accessing a database using the higher level Lighter API

// Open a SQLite database embedded in the module resources:
let db = ContactsDB.module!

// Fetch the number of records:
print("Total number of people stored:", 
      try db.people.fetchCount())

// There are various ways to filter, including a plain Swift closure:
let people = try db.people.filter { person in
  person.title == nil
}

// Primary & foreign keys are directly supported:
let person    = try db.people.find(1)
let addresses = try db.addresses.fetch(for: person)

// Updates can be done one-shot or, better, using a transaction:
try await db.transaction { tx in
  var person = try tx.people.find(2)!
  
  // Update a record.
  person.title = "ZEO"
  try tx.update(person)

  // Delete a record.
  try tx.delete(person)
  
  // Reinsert thew same record
  let newPerson = try tx.insert(person) // gets new ID!
}


#### Fetching Individual Columns

One of the advantages of SQL is that individual columns can be selected
and updated for maximum efficiency. Only things that are
required need to be fetched (vs. full records):
```swift
// Fetch just the `id` and `name` columns:
let people = try await db.select(from: \.people, \.id, \.name) {
  $0.id > 2 && $0.title == nil
}

// Bulk update a specific column:
try db.update(\.people, set: \.title, to: nil, where: { record in
  record.name.hasPrefix("Duck")
})

The references are fully typesafe down to the schema, only columns contained in the person table can be specified.

Dependency free SQLite3 API

The toolkit is also useful for cases in which the extra dependency on Lighter is not desirable. For such the generator can produce database specific Swift APIs that work alongside the regular SQLite API.

// Open the database, can also just use `sqlite3_open_v2`:
var db : OpaquePointer!
sqlite3_open_contacts("contacts.db", &db)
defer { sqlite3_close(db) }

// Fetch a person by primary key:
let person = sqlite3_person_find(db, 2)
  
// Fetch and filter people:
let people = sqlite3_people_fetch(db) {
  $0.name.hasPrefix("Ja")
}

// Insert a record
var person = Person(id: 0, name: "Jason Bourne")
sqlite3_person_insert(db, &person)

There is another style the code generator can produce, it attaches the same functions to the generated types, e.g.:

let people = Person.fetch(in: db) { $0.name.hasPrefix("So") }
var person = Person.find(2, in: db)

person.name = "Bourne"
person.update(in: db)
person.delete(in: db)
person.insert(into: db)

The main advantage of using the raw API is that no extra dependency is necessary at all. The generated functions are completely self-contained and can literally be copied&pasted into places where needed.

Beautiful, autogenerated DocC API Comments

The Lighter code generator can also generate API comments for the database types.

Example: Northwind Database.

Interested? 👉 Getting Started.

Who

Lighter is brought to you by Helge Heß / ZeeZide. We like feedback, GitHub stars, cool contract work, presumably any form of praise you can think of.

Want to support my work? Buy an app: Code for SQLite3, Past for iChat, SVG Shaper, HMScriptEditor. You don’t have to use it! 😀

Written on August 16, 2022