This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We use Swift 5's new string interpolation API to automatically insert placeholders in SQL queries.

00:06 Today we'll look at the new string interpolation APIs from Swift 5, and we'll try them out by building SQL queries with placeholders for parameters.

00:18 We rely on PostgreSQL to prevent SQL injection by properly escaping the parameters we pass into our queries. We wrote an initializer on Query, and the initializer automatically builds up the query string with a placeholder — in the form of $1, $2, etc. — for each of values that need to be escaped.

Building a Query String

00:52 To illustrate this, let's look at a simplified version of our backend code, written in Swift 4.2:

typealias SQLValue = String

struct Query<A> {
    let sql: String
    let values: [SQLValue]
    let parse: ([SQLValue]) -> A
    
    typealias Placeholder = String
    
    init(values: [SQLValue], build: ([Placeholder]) -> String, parse: @escaping ([SQLValue]) -> A) {
        let placeholders = values.enumerated().map { "$\($0.0 + 1)" }
        self.values = values
        self.sql = build(placeholders)
        self.parse = parse
    }
}

01:28 Let's create an example query that retrieves a user by their ID. The initializer takes both an array of values and a build function that creates the query string from generated placeholders. This build function receives a placeholder for each of the values we pass in:

let id = "1234"
let sample = Query<String>(values: [id], build: { params in
    "SELECT * FROM users WHERE id=\(params[0])"
}, parse: { $0[0] })

assert(sample.sql == "SELECT * FROM users WHERE id=$1")
assert(sample.values == ["1234"])

Using String Interpolation

01:51 Swift 5 makes string interpolation public, which means we can implement our own type of interpolation that automatically inserts placeholders for values. This will allow us to create our query without using a build function:

let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id)", parse: { $0[0] })

02:46 In order to make this work, the type of the query parameter we pass into Query has to conform to the ExpressibleByStringInterpolation protocol. Of course, we could use String as the type — and then we wouldn't have to use the param: label — but we want to be explicit about parameters that need to be escaped (and therefore need a placeholder), so we create a custom type, QueryPart:

struct QueryPart {
    let sql: String
    let values: [SQLValue]
}

struct Query<A> {
    let query: QueryPart
    let parse: ([SQLValue]) -> A

    init(_ part: QueryPart, parse: @escaping ([SQLValue]) -> A) {
        self.query = part
        self.parse = parse
    }
}

04:26 Next, we need to make QueryPart conform to both ExpressibleByStringLiteral and ExpressibleByStringInterpolation:

extension QueryPart: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        self.sql = value
        self.values = []
    }
}

extension QueryPart: ExpressibleByStringInterpolation {
}

05:20 The last extension already compiles because the protocol has a default implementation, i.e. the interpolation type from the standard library:

public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {

    /// The type each segment of a string literal containing interpolations
    /// should be appended to.
    associatedtype StringInterpolation : StringInterpolationProtocol = DefaultStringInterpolation where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType

    // ...
}

05:45 We want to override this default implementation by specifying our own type conforming to StringInterpolationProtocol, and this will be the type for each segment we append to QueryPart:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    // ...
}

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPartStringInterpolation
}

06:45 This new interpolation type is where we implement the custom behavior we want when we insert a value into our query string. The first thing we have to implement is a required initializer, which doesn't need to do anything in our case:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    init(literalCapacity: Int, interpolationCount: Int) {
    }
}

07:08 The way string interpolation works is that we'll be called with each segment that needs to be appended — i.e. string literals and interpolations. To keep track of what we receive, we'll need the same two properties that QueryPart has:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    var sql: String = ""
    var values: [SQLValue] = []
    
    init(literalCapacity: Int, interpolationCount: Int) {
    }
}

08:00 The next step is adding the various appending methods. The first one appends a string literal:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    var sql: String = ""
    var values: [SQLValue] = []
    
    // ...

    mutating func appendLiteral(_ literal: String) {
        sql += literal
    }
}

08:22 The second method is for appending SQL values, and we give it the parameter label that corresponds with our call site. Inside the method, we first append the received value to our array of values, and then we append a new placeholder to the query string:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    var sql: String = ""
    var values: [SQLValue] = []
    
    // ...

    mutating func appendInterpolation(param value: SQLValue) {
        sql += "$\(values.count + 1)"
        values.append(value)
    }
}

09:47 On QueryPart, we have to add the initializer that takes a QueryPartStringInterpolation:

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPartStringInterpolation

    init(stringInterpolation: QueryPartStringInterpolation) {
        self.sql = stringInterpolation.sql
        self.values = stringInterpolation.values
    }
}

10:34 Now the code compiles and we can check that our sample query is built correctly:

let id = "1234"
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id)", parse: { $0[0] })

assert(sample.query.sql == "SELECT * FROM users WHERE id=$1")
assert(sample.query.values == ["1234"])

11:10 It works! Our query string has a placeholder for the ID value, and the values array contains the ID. Let's try adding another value:

let id = "1234"
let email = "mail@objc.io"
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id) AND email=\(email)", parse: { $0[0] })

11:27 This doesn't compile because we forgot the param: label, and that's actually a good thing: we don't want to insert arbitrary strings. After we add the label, we test that the Query is built up the way we expect:

assert(sample.query.sql == "SELECT * FROM users WHERE id=$1 AND email=$2")
assert(sample.query.values == [id, email])

Inserting Raw Strings

12:06 In the actual codebase of our backend, we dynamically generate queries from Codable types, and these types supply the table name that should be used. So we also want to be able to dynamically insert a table name into our query:

let tableName = "users"
let sample = Query<String>("SELECT * FROM \(raw: tableName) WHERE id=\(param: id) AND email=\(param: email)", parse: { $0[0] })

12:48 This segment doesn't have to be escaped, and again we want to be explicit about this, in order to avoid accidentally inserting a random string into the query. So we use the label raw: for this interpolation:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    // ...

    mutating func appendInterpolation(raw value: String) {
        sql += value
    }
}

Simplifying Types

13:19 We can clean up our code by simplifying the types we use. We've made QueryPart conform to ExpressibleByStringInterpolation, and then we introduced QueryPartStringInterpolation as the string interpolation type. But rather than having two separate types with duplicate properties, we can use QueryPart itself for the string interpolation:

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPart

    init(stringInterpolation: QueryPart) {
        self.sql = stringInterpolation.sql
        self.values = stringInterpolation.values
    }
}

14:37 The two properties on QueryPart have to become mutable:

struct QueryPart {
    var sql: String
    var values: [SQLValue]
}

14:56 Then we initialize them in the required initializer:

extension QueryPart: StringInterpolationProtocol {
    init(literalCapacity: Int, interpolationCount: Int) {
        self.sql = ""
        self.values = []
    }

    // ...
}

15:14 And that's all we have to do to eliminate the need for the separate type, QueryPartStringInterpolation.

Appending Clauses

15:23 In our backend, we can construct a base query that finds a record by ID, like today's sample query, and then we have the possibility of appending clauses to that base query. This way, we can specify additional filtering (with another condition) or sorting (by appending an ORDER BY clause) without having to write the base query twice.

15:58 For this to work, we have to add an appending method to Query itself. Let's add that feature in the next episode.

16:14 We're excited about the possibilities that string interpolation brings us. It's a completely new tool, and as a community, we still have to figure out all the things we can do with it.

Resources

  • Sample Project

    Written in Swift 5

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

57 Episodes · 20h06min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes