add swiftUI code
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
import Foundation
|
||||
|
||||
extension Database {
|
||||
|
||||
// MARK: - Statements
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.makeStatement(sql: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.makeStatement(sql: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func makeStatement(sql: String) throws -> Statement {
|
||||
try makeStatement(sql: sql, prepFlags: 0)
|
||||
}
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.makeStatement(literal: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.makeStatement(literal: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. An error is raised otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?
|
||||
/// """)
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is not set, but second is)
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player
|
||||
/// WHERE color = ? AND score > \(1000)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func makeStatement(literal sqlLiteral: SQL) throws -> Statement {
|
||||
let (sql, arguments) = try sqlLiteral.build(self)
|
||||
let statement = try makeStatement(sql: sql)
|
||||
if arguments.isEmpty == false {
|
||||
// Throws if arguments do not match
|
||||
try statement.setArguments(arguments)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?", prepFlags: 0)
|
||||
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
|
||||
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
|
||||
/// SQLite 3.20.0, see <http://www.sqlite.org/c3ref/prepare.html>)
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
func makeStatement(sql: String, prepFlags: CUnsignedInt) throws -> Statement {
|
||||
let statements = SQLStatementCursor(database: self, sql: sql, arguments: nil, prepFlags: prepFlags)
|
||||
guard let statement = try statements.next() else {
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_ERROR,
|
||||
message: "empty statement",
|
||||
sql: sql)
|
||||
}
|
||||
do {
|
||||
guard try statements.next() == nil else {
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: """
|
||||
Multiple statements found. To execute multiple statements, use \
|
||||
Database.execute(sql:) or Database.allStatements(sql:) instead.
|
||||
""",
|
||||
sql: sql)
|
||||
}
|
||||
} catch {
|
||||
// Something while would not compile was found after the first statement.
|
||||
// Complain about multiple statements anyway.
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: """
|
||||
Multiple statements found. To execute multiple statements, use \
|
||||
Database.execute(sql:) or Database.allStatements(sql:) instead.
|
||||
""",
|
||||
sql: sql)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.cachedStatement(sql: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.cachedStatement(sql: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// The returned statement may have already been used: it may or may not
|
||||
/// contain values for its eventual arguments.
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func cachedStatement(sql: String) throws -> Statement {
|
||||
try publicStatementCache.statement(sql)
|
||||
}
|
||||
|
||||
/// Returns a prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.cachedStatement(literal: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.cachedStatement(literal: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. An error is raised otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?
|
||||
/// """)
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is not set, but second is)
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player
|
||||
/// WHERE color = ? AND score > \(1000)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func cachedStatement(literal sqlLiteral: SQL) throws -> Statement {
|
||||
let (sql, arguments) = try sqlLiteral.build(self)
|
||||
let statement = try cachedStatement(sql: sql)
|
||||
if arguments.isEmpty == false {
|
||||
// Throws if arguments do not match
|
||||
try statement.setArguments(arguments)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a cached statement that does not conflict with user's cached statements.
|
||||
func internalCachedStatement(sql: String) throws -> Statement {
|
||||
try internalStatementCache.statement(sql)
|
||||
}
|
||||
|
||||
/// Returns a cursor of prepared statements.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statements = try db.allStatements(sql: """
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
|
||||
/// while let statement = try statements.next() {
|
||||
/// try statement.execute()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `arguments` parameter must be nil, or all arguments must be set. The
|
||||
/// returned cursor will throw an error otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
///
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """, arguments: [1000, 1000])
|
||||
///
|
||||
/// // NOT OK (first argument is set, but second is not)
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """, arguments: [1000])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - returns: A cursor of prepared ``Statement``.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func allStatements(sql: String, arguments: StatementArguments? = nil)
|
||||
throws -> SQLStatementCursor
|
||||
{
|
||||
SQLStatementCursor(database: self, sql: sql, arguments: arguments)
|
||||
}
|
||||
|
||||
/// Returns a cursor of prepared statements.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statements = try db.allStatements(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"));
|
||||
/// INSERT INTO player (name) VALUES (\("Barbara"));
|
||||
/// INSERT INTO player (name) VALUES (\("O'Brien"));
|
||||
/// """)
|
||||
/// while let statement = try statements.next() {
|
||||
/// try statement.execute()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. The returned cursor will throw an error otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
///
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000);
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is set, but second is not)
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A cursor of prepared ``Statement``.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func allStatements(literal sqlLiteral: SQL) throws -> SQLStatementCursor {
|
||||
let context = SQLGenerationContext(self)
|
||||
let sql = try sqlLiteral.sql(context)
|
||||
let arguments = context.arguments.isEmpty
|
||||
? nil // builds statements without arguments
|
||||
: context.arguments // force arguments to match
|
||||
return SQLStatementCursor(database: self, sql: sql, arguments: arguments)
|
||||
}
|
||||
|
||||
/// Executes one or several SQL statements.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.execute(
|
||||
/// sql: "INSERT INTO player (name) VALUES (:name)",
|
||||
/// arguments: ["name": "Arthur"])
|
||||
///
|
||||
/// try db.execute(sql: """
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func execute(sql: String, arguments: StatementArguments = StatementArguments()) throws {
|
||||
try execute(literal: SQL(sql: sql, arguments: arguments))
|
||||
}
|
||||
|
||||
/// Executes one or several SQL statements.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.execute(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"))
|
||||
/// """)
|
||||
///
|
||||
/// try db.execute(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"));
|
||||
/// INSERT INTO player (name) VALUES (\("Barbara"));
|
||||
/// INSERT INTO player (name) VALUES (\("O'Brien"));
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func execute(literal sqlLiteral: SQL) throws {
|
||||
let statements = try allStatements(literal: sqlLiteral)
|
||||
while let statement = try statements.next() {
|
||||
try statement.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cursor over all statements in an SQL string.
|
||||
public class SQLStatementCursor {
|
||||
private let database: Database
|
||||
private let cString: ContiguousArray<CChar>
|
||||
private let prepFlags: CUnsignedInt
|
||||
private let initialArgumentCount: Int?
|
||||
|
||||
// Mutated by iteration
|
||||
private var offset: Int // offset in the C string
|
||||
private var arguments: StatementArguments? // Nil when arguments are set later
|
||||
|
||||
init(database: Database, sql: String, arguments: StatementArguments?, prepFlags: CUnsignedInt = 0) {
|
||||
self.database = database
|
||||
self.cString = sql.utf8CString
|
||||
self.prepFlags = prepFlags
|
||||
self.initialArgumentCount = arguments?.values.count
|
||||
self.offset = 0
|
||||
self.arguments = arguments
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit non-conformance to Sendable: database cursors must be used from
|
||||
// a serialized database access dispatch queue.
|
||||
@available(*, unavailable)
|
||||
extension SQLStatementCursor: Sendable { }
|
||||
|
||||
extension SQLStatementCursor: Cursor {
|
||||
public func next() throws -> Statement? {
|
||||
guard offset < cString.count - 1 /* trailing \0 */ else {
|
||||
// End of C string -> end of cursor.
|
||||
try checkArgumentsAreEmpty()
|
||||
return nil
|
||||
}
|
||||
|
||||
return try cString.withUnsafeBufferPointer { buffer in
|
||||
let baseAddress = buffer.baseAddress! // never nil because the buffer contains the trailing \0.
|
||||
|
||||
// Compile next statement
|
||||
var statementEnd: UnsafePointer<CChar>? = nil
|
||||
let statement = try Statement(
|
||||
database: database,
|
||||
statementStart: baseAddress + offset,
|
||||
statementEnd: &statementEnd,
|
||||
prepFlags: prepFlags)
|
||||
|
||||
// Advance to next statement
|
||||
offset = statementEnd! - baseAddress // never nil because statement compilation did not fail.
|
||||
|
||||
guard let statement else {
|
||||
// No statement found -> end of cursor.
|
||||
try checkArgumentsAreEmpty()
|
||||
return nil
|
||||
}
|
||||
|
||||
if arguments != nil {
|
||||
// Extract statement arguments
|
||||
let bindings = try arguments!.extractBindings(
|
||||
forStatement: statement,
|
||||
allowingRemainingValues: true)
|
||||
// unchecked is OK because we just extracted the correct
|
||||
// number of arguments
|
||||
statement.setUncheckedArguments(StatementArguments(bindings))
|
||||
}
|
||||
|
||||
return statement
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that all arguments were consumed: it is a programmer error to
|
||||
/// provide arguments that do not match the statements.
|
||||
private func checkArgumentsAreEmpty() throws {
|
||||
if let arguments,
|
||||
let initialArgumentCount,
|
||||
arguments.values.isEmpty == false
|
||||
{
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: "wrong number of statement arguments: \(initialArgumentCount)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Database {
|
||||
/// Makes sure statement can be executed, and prepares database observation.
|
||||
@usableFromInline
|
||||
func statementWillExecute(_ statement: Statement) throws {
|
||||
// Aborted transactions prevent statement execution (see the
|
||||
// documentation of this method for more information).
|
||||
try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments)
|
||||
|
||||
// Suspended databases must not execute statements that create the risk
|
||||
// of `0xdead10cc` exception (see the documentation of this method for
|
||||
// more information).
|
||||
try checkForSuspensionViolation(from: statement)
|
||||
|
||||
// Record the database region selected by the statement execution.
|
||||
try registerAccess(to: statement.databaseRegion)
|
||||
|
||||
// Database observation: prepare transaction observers.
|
||||
observationBroker?.statementWillExecute(statement)
|
||||
}
|
||||
|
||||
/// May throw a cancelled commit error, if a transaction observer cancels
|
||||
/// an empty transaction.
|
||||
@usableFromInline
|
||||
func statementDidExecute(_ statement: Statement) throws {
|
||||
if statement.invalidatesDatabaseSchemaCache {
|
||||
clearSchemaCache()
|
||||
}
|
||||
|
||||
checkForAutocommitTransition()
|
||||
|
||||
// Database observation: cleanup
|
||||
try observationBroker?.statementDidExecute(statement)
|
||||
}
|
||||
|
||||
/// Always throws an error
|
||||
@usableFromInline
|
||||
func statementDidFail(_ statement: Statement, withResultCode resultCode: CInt) throws -> Never {
|
||||
// Failed statements can not be reused, because `sqlite3_reset` won't
|
||||
// be able to restore the statement to its initial state:
|
||||
// https://www.sqlite.org/c3ref/reset.html
|
||||
//
|
||||
// So make sure we clear this statement from the cache.
|
||||
internalStatementCache.remove(statement)
|
||||
publicStatementCache.remove(statement)
|
||||
|
||||
checkForAutocommitTransition()
|
||||
|
||||
// Extract values that may be modified by the user in their
|
||||
// `TransactionObserver.databaseDidRollback(_:)` implementation
|
||||
// (see below).
|
||||
let message = lastErrorMessage
|
||||
let arguments = statement.arguments
|
||||
|
||||
// Database observation: cleanup.
|
||||
//
|
||||
// If the statement failure is due to a transaction observer that has
|
||||
// cancelled a transaction, this calls `TransactionObserver.databaseDidRollback(_:)`,
|
||||
// and throws the user-provided cancelled commit error.
|
||||
try observationBroker?.statementDidFail(statement)
|
||||
|
||||
// Throw statement failure
|
||||
throw DatabaseError(
|
||||
resultCode: resultCode,
|
||||
message: message,
|
||||
sql: statement.sql,
|
||||
arguments: arguments,
|
||||
publicStatementArguments: configuration.publicStatementArguments)
|
||||
}
|
||||
|
||||
private func checkForAutocommitTransition() {
|
||||
if sqlite3_get_autocommit(sqliteConnection) == 0 {
|
||||
if autocommitState == .on {
|
||||
// Record transaction date as soon as the connection leaves
|
||||
// auto-commit mode.
|
||||
// We grab a result, so that this failure is later reported
|
||||
// whenever the user calls `Database.transactionDate`.
|
||||
transactionDateResult = Result { try configuration.transactionClock.now(self) }
|
||||
}
|
||||
autocommitState = .off
|
||||
} else {
|
||||
if autocommitState == .off {
|
||||
// Reset transaction date
|
||||
transactionDateResult = nil
|
||||
}
|
||||
autocommitState = .on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread-unsafe statement cache
|
||||
struct StatementCache {
|
||||
unowned let db: Database
|
||||
private var statements: [String: Statement] = [:]
|
||||
|
||||
init(database: Database) {
|
||||
self.db = database
|
||||
}
|
||||
|
||||
mutating func statement(_ sql: String) throws -> Statement {
|
||||
if let statement = statements[sql] {
|
||||
return statement
|
||||
}
|
||||
|
||||
// http://www.sqlite.org/c3ref/c_prepare_persistent.html#sqlitepreparepersistent
|
||||
// > The SQLITE_PREPARE_PERSISTENT flag is a hint to the query
|
||||
// > planner that the prepared statement will be retained for a long
|
||||
// > time and probably reused many times.
|
||||
//
|
||||
// This looks like a perfect match for cached statements.
|
||||
//
|
||||
// However SQLITE_PREPARE_PERSISTENT was only introduced in
|
||||
// SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
|
||||
#else
|
||||
let statement: Statement
|
||||
if #available(iOS 12, macOS 10.14, watchOS 5, *) { // SQLite 3.20+
|
||||
statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
|
||||
} else {
|
||||
statement = try db.makeStatement(sql: sql)
|
||||
}
|
||||
#endif
|
||||
statements[sql] = statement
|
||||
return statement
|
||||
}
|
||||
|
||||
mutating func clear() {
|
||||
statements = [:]
|
||||
}
|
||||
|
||||
mutating func remove(_ statement: Statement) {
|
||||
statements.removeFirst { $0.value === statement }
|
||||
}
|
||||
|
||||
mutating func removeAll(where shouldBeRemoved: (Statement) -> Bool) {
|
||||
statements = statements.filter { (_, statement) in !shouldBeRemoved(statement) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user