Swift 5 using a new string interpolation API automatically insert a placeholder in a SQL query

Today we will take a look at the new Swift 5 string interpolation API, we will try them by building SQL queries using placeholders.

We rely on our PostgreSQL escaping through the correct parameters passed to the query to prevent SQL injection. We write an initialization Query, and initialization of the query string automatically create placeholders - forms $1, $2and so on - for every need escape value.

Construction of the query string

To illustrate this point, let's look at a simplified version of the back-end code written by 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
    }
}
复制代码

Let's create a sample query, retrieve the ID by its users. Initializing an array of values and accepts a buildcreate function generated from the query string placeholder. This buildfunction receives a placeholder we pass each value:

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"])
复制代码

String interpolation

Swift 5 the string interpolation public, which means that we can achieve our own interpolation type, automatically insert a placeholder value. This will allow us to use without build creating a query function in the case of:

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

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
    }
}
复制代码

Next, we need QueryPartto comply with both  ExpressibleByStringLiteraland ExpressibleByStringInterpolation:

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

extension QueryPart: ExpressibleByStringInterpolation {
}
复制代码

The last extension has been compiled since the protocol has a default implementation that type of interpolation in 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

    // ... }
复制代码

We want to override this default by specifying our own type of accord StringInterpolationProtocol, which will be added to our type of each segment QueryPart:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    // ... }

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPartStringInterpolation
}
复制代码

This new type of interpolation is that we implement custom behavior we want in the query string value is inserted into place. The first thing that is required to initialize the program we have implemented in our case does not need to do anything:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    init(literalCapacity: Int, interpolationCount: Int) {
    }
}
复制代码

String interpolation works is that we will need additional calls each segment - the string literals and interpolation. To track what we received, we need to have the same two properties QueryPart:

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

    init(literalCapacity: Int, interpolationCount: Int) {
    }
}
复制代码

The next step is to add a variety of additional methods. First attach a string literal:

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

    // ... 
    mutating func appendLiteral(_ literal: String) {
        sql += literal
    }
}
复制代码

The second method is to attach SQL value, we give it a corresponding parameter of our call site label. Within a method, we first received value to the value of our array, and then attach a new placeholder in 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)
    }
}
复制代码

In QueryPart, we must add the initialization procedure, it requires QueryPartStringInterpolation:

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPartStringInterpolation

    init(stringInterpolation: QueryPartStringInterpolation) {
        self.sql = stringInterpolation.sql
        self.values = stringInterpolation.values
    }
}
复制代码

10:34 Now compile the code, we can check our sample query is constructed 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"])
复制代码

It works! We have a query string ID value placeholder valuesarray containing the ID. Let's try to add another value:

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

This does not compile because we forget the param:label, which is actually a good thing: we do not want to insert any string. After we add tags, we test it Queryin accordance with the way we expect construction:

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

Insert the original string

In practice, our back-end code base, we have the Codabledynamically generated query types, which provide a table name should be used. So we also want to be able to dynamically insert a table name in the query:

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

This section does not have to escape, we want to make this clear once again, to avoid accidentally insert a random string in the query. So we use labels raw:to interpolate:

struct QueryPartStringInterpolation: StringInterpolationProtocol {
    // ... 
    mutating func appendInterpolation(raw value: String) {
        sql += value
    }
}
复制代码

Simplified type

We can clean up the code by simplifying the type we use. We have been QueryPartin line with ExpressibleByStringInterpolation, and then we introduced QueryPartStringInterpolationa string interpolation type. However, we can QueryPartown a string for interpolation, instead of having two separate types duplicate attributes:

extension QueryPart: ExpressibleByStringInterpolation {
    typealias StringInterpolation = QueryPart

    init(stringInterpolation: QueryPart) {
        self.sql = stringInterpolation.sql
        self.values = stringInterpolation.values
    }
}
复制代码

These two properties QueryParthave become variable:

struct QueryPart {
    var sql: String
    var values: [SQLValue]
}
复制代码

Then we initialize them in the desired initialization routine:

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

    // ... }
复制代码

This is what we have to do is eliminate the need for a separate type QueryPartStringInterpolation.

additional terms

In our back-end, we can build a basic query, to find records by ID, the same as today's example query, and then we can be attached to the basic query clause. In this way, we can specify additional filtering (using different criteria) or sort (by additional ORDER BYclause), without having to write basic queries twice.

To do this, we must add an additional method for themselves Query . Let's focus on adding this feature in the next.

We bring our string interpolation excited about the possibility. This is a brand new tool, as a community, we still need to figure out all the things we can do with it.


Reprinted from: www.jianshu.com/p/a9d29bf11...

Reproduced in: https: //juejin.im/post/5cfe4d156fb9a07eee5ec321

Guess you like

Origin blog.csdn.net/weixin_33918357/article/details/93182115
Recommended