SwiftQL

Swift Query Language allows you to write type-safe relational database queries using familiar Swift syntax.

The primary goal of SwiftQL is to allow SQL queries to be written in a type-safe manner, so that SQL queries can be verified at compile time, to reduce runtime errors and security issues caused by common mistakes such as incorrect syntax, or improperly escaped input.

SwiftQL uses SQLite and as such attempts to adhere to SQL as understood by SQLite within reason.

SwiftQL is currently only a proof of concept and is not ready for use in production systems.

If you are intereted in using SwiftQL in your project please consider contributing or donating.

Tutorial

SwiftQL works with relational databases. Objects in SwiftQL are derived from tables.

To use an object in SwiftQL, you first need to define meta-data which describes how each object is stored in the database.

At present this definition needs to be implemented by hand.

A future goal of SwiftQL is to derive the schema automatically from Swift objects.

First define the types you would like to use in your database.

Value types are recommended, although reference types can also be used.

SwiftQL currently does not support enums or protocols.

struct Sample: Identifiable, Equatable {
    
    let id: String
    let value: Int
}

Next we create a custom class conforming to the Database protocol, which describes the schema or layout of the database.

Implement the Schema associated type, which extends from the DatabaseSchema.

class MyDatabase: Database {
    final class Schema: DatabaseSchema {

Within the schema class we need to define a class which describes the schema for each table in the database. In this example we define a schema for the Samples table.

We need to define a property on the class for each field in the table. Our Sample table has an id primary key field which is a string, and value field which is an integer.

SwiftQL supports the following types for fields:

Boolean: Stored as 64-bit integer, where false is stored as 0 and true is stored as 1. Int: Stored as 64-bit integer. Double: Stored as IEEE double precision floating point. String: Stored as UTF-8 encoded null terminated string. Data: Stored as raw bytes. URL: Stored as text using the .absoluteString representation. UUID: Stored as text using the .uuidString representation. Date: Stored as text using ISO8601 representation

Each field requires the name of the column in the table.

We also need to define the tableName which is the name used in SQL queries.

tableFields should return an array containing all of the fields for the table. This is used by SwiftQL to process some queries, such as table create statements.

The entity(from:) method should return an instance of the entity stored in the table, deserialized from the SQLRow provided in the parameter. This method is used by SwiftQL to instantiate the entity when returning a result from a select query.

Finally, the values(entity:) method should return a dictionary that represents an instance of the Swift entity in a form that is understood by SQL.

        final class SampleSchema: BaseTableSchema, TableSchema {
        
            lazy var id = PrimaryKeyField<String>(name: "id", table: self)
            lazy var value = Field<Int>(name: "value", table: self)
            
            static let tableName = SQLIdentifier(stringLiteral: "samples")

            var tableFields: [AnyField] {
                return [id, value]
            }
            
            func entity(from row: SQLRow) -> Sample {
                Sample(
                    id: row.field(id),
                    value: row.field(value)
                )
            }
            
            func values(entity: Sample) -> [SQLIdentifier : SQLExpression] {
                [
                    id.name: StringLiteral(entity.id),
                    value.name: IntegerLiteral(entity.value)
                ]
            }
        }

Lastly we need to provide a variable to access the schema, and a constructor to instantiate the database.


        func samples() -> SampleSchema {
            schema(table: SampleSchema.self)
        }
    }
    
    let connection: SQLite.Connection
    
    init(connection: SQLite.Connection) {
        self.connection = connection
    }
}
}

Once we have the database definition in place we can instantiate the database and start to use it. The code below opens the database file named mydatabase.sqlite3 in the cache directory.

let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let fileURL = directory.appendingPathComponent("mydatabase").appendingPathExtension("sqlite3")
let resource = SQLite.Resource(fileURL: fileURL)
let connection = try resource.connect()
let database = MyDatabase(connection: connection)

Create each table:

try database.execute { db in
    // CREATE TABLE IF NOT EXISTS `samples`
    // `id` TEXT PRIMARY KEY NOT NULL,
    // `value` INT NOT NULL
    Create(db.samples())
}

Insert data into the database:

try database.execute { db in
    // INSERT INTO `samples`
    // ( `id`, `value` )
    // VALUES ( ?1, ?2 )"
    let sample = db.samples()
    Insert(sample, Sample(id: "foo", value: 7))
}

Fetch data from the database

try database.execute { db in
    // SELECT `t0`.`id`, `t0`.`value`
    // FROM `samples` AS `t0`
    let sample = db.samples()
    Select(sample)
    From(sample)
}

Note in the above examples that we used a variable sample to refer to the schema. This is used when we need to refer to the same table multiple times in a query. In queries where a table is used only once, such as in the insert example, the table instance can be used directly without assigning it to a variable.

TODO:

Below are some of the goals for SwiftQL, roughly in order of highest priority first.

  • Observable live updating select query results.
  • Define table schemas using the Create statement instead of as metadata classes.
  • SQL GROUP BY syntax.
  • Handle SQLITE_BUSY errors.
  • Allow only read (select) statements to be observed.
  • Optimize observable statements: Only re-query when the tables and fields mentioned in the query are changed.
  • Bulk import from CSV and data frames.
  • Automatic schema migration when opening a populated database which has an outdated schema.
  • Migrate documentation to Swift-DocC.
  • Interface to other databases including Postgres and MySQL.

License:

SwiftQL is free for commercial and non-commercial use.

Attribution to the original source is required.

GitHub

View Github