Merge branch 'feature/refactoring' into develop

This commit is contained in:
2025-10-25 18:33:14 +03:00
13 changed files with 20 additions and 1614 deletions

View File

@@ -48,8 +48,6 @@ public final class Connection {
/// Initializes a new connection to an SQLite database. /// Initializes a new connection to an SQLite database.
/// ///
/// Opens a connection to the database at the specified `location` using the given `options`. /// Opens a connection to the database at the specified `location` using the given `options`.
/// If the location represents a file path, this method ensures that the parent directory
/// exists, creating intermediate directories if needed.
/// ///
/// ### Example /// ### Example
/// ///
@@ -73,15 +71,7 @@ public final class Connection {
/// ///
/// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to /// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to
/// SQLite-related issues such as invalid path, missing permissions, or corruption. /// SQLite-related issues such as invalid path, missing permissions, or corruption.
/// - Throws: An error if directory creation fails for file-based database locations. public init(location: Location, options: Options) throws(SQLiteError) {
public init(location: Location, options: Options) throws {
if case let Location.file(path) = location, !path.isEmpty {
try FileManager.default.createDirectory(
at: URL(fileURLWithPath: path).deletingLastPathComponent(),
withIntermediateDirectories: true
)
}
var connection: OpaquePointer! = nil var connection: OpaquePointer! = nil
let status = sqlite3_open_v2(location.path, &connection, options.rawValue, nil) let status = sqlite3_open_v2(location.path, &connection, options.rawValue, nil)
@@ -121,8 +111,7 @@ public final class Connection {
/// ///
/// - Throws: ``SQLiteError`` if the connection cannot be opened due to SQLite-level errors, /// - Throws: ``SQLiteError`` if the connection cannot be opened due to SQLite-level errors,
/// invalid path, missing permissions, or corruption. /// invalid path, missing permissions, or corruption.
/// - Throws: An error if the required directory structure cannot be created. public convenience init(path: String, options: Options) throws(SQLiteError) {
public convenience init(path: String, options: Options) throws {
try self.init(location: .file(path: path), options: options) try self.init(location: .file(path: path), options: options)
} }
@@ -214,8 +203,8 @@ extension Connection: ConnectionProtocol {
try Statement(db: connection, sql: query, options: options) try Statement(db: connection, sql: query, options: options)
} }
public func execute(raw sql: String) throws(SQLiteError) { public func execute(sql script: String) throws(SQLiteError) {
let status = sqlite3_exec(connection, sql, nil, nil, nil) let status = sqlite3_exec(connection, script, nil, nil, nil)
guard status == SQLITE_OK else { throw SQLiteError(connection) } guard status == SQLITE_OK else { throw SQLiteError(connection) }
} }
} }

View File

@@ -13,7 +13,7 @@ extension Function {
/// ) /// )
/// try connection.add(function: Function.Regexp.self) /// try connection.add(function: Function.Regexp.self)
/// ///
/// try connection.execute(raw: """ /// try connection.execute(sql: """
/// SELECT * FROM users WHERE name REGEXP 'John.*'; /// SELECT * FROM users WHERE name REGEXP 'John.*';
/// """) /// """)
/// ``` /// ```

View File

@@ -49,7 +49,7 @@ Use `nil` or `"main"` for the primary database, `"temp"` for the temporary one,
others. others.
```swift ```swift
try connection.execute(raw: "ATTACH DATABASE 'analytics.db' AS analytics") try connection.execute(sql: "ATTACH DATABASE 'analytics.db' AS analytics")
try connection.apply(.passphrase("aux-password"), name: "analytics") try connection.apply(.passphrase("aux-password"), name: "analytics")
``` ```

View File

@@ -25,13 +25,12 @@ SQLite messages verbatim.
Most DataLiteCore APIs are annotated as `throws(SQLiteError)`, meaning they only throw SQLiteError Most DataLiteCore APIs are annotated as `throws(SQLiteError)`, meaning they only throw SQLiteError
instances. instances.
Only APIs that touch the file system or execute arbitrary user code may throw other error Only APIs that execute arbitrary user code or integrate with external systems may surface other
types — for example, ``Connection/init(location:options:)`` when creating directories for on-disk error types. Consult the documentation on each API for specific details.
databases.
```swift ```swift
do { do {
try connection.execute(raw: """ try connection.execute(sql: """
INSERT INTO users(email) VALUES ('ada@example.com') INSERT INTO users(email) VALUES ('ada@example.com')
""") """)
} catch { } catch {
@@ -48,8 +47,8 @@ do {
## Multi-Statement Scenarios ## Multi-Statement Scenarios
- ``ConnectionProtocol/execute(sql:)`` and ``ConnectionProtocol/execute(raw:)`` stop at the first - ``ConnectionProtocol/execute(sql:)`` stops at the first failing statement and propagates its
failing statement and propagate its ``SQLiteError``. ``SQLiteError``.
- ``StatementProtocol/execute(_:)`` reuses prepared statements; inside `catch` blocks, remember to - ``StatementProtocol/execute(_:)`` reuses prepared statements; inside `catch` blocks, remember to
call ``StatementProtocol/reset()`` and (if needed) ``StatementProtocol/clearBindings()`` before call ``StatementProtocol/reset()`` and (if needed) ``StatementProtocol/clearBindings()`` before
retrying. retrying.

View File

@@ -1,98 +0,0 @@
# Running SQL Scripts
Execute and automate SQL migrations, seed data, and test fixtures with DataLiteCore.
``SQLScript`` and ``ConnectionProtocol/execute(sql:)`` let you run sequences of prepared statements
as a single script. The loader splits content by semicolons, removes comments and whitespace, and
compiles each statement individually. Scripts run in autocommit mode by default — execution stops at
the first failure and throws the corresponding ``SQLiteError``.
## Building Scripts
Create a script inline or load it from a bundled resource. ``SQLScript`` automatically strips
comments and normalizes whitespace.
```swift
let script = SQLScript(string: """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE
);
INSERT INTO users (username) VALUES ('ada'), ('grace');
""")
```
Load a script from your module or app bundle using ``SQLScript/init(byResource:extension:in:)``:
```swift
let bootstrap = try? SQLScript(
byResource: "bootstrap",
extension: "sql",
in: .module
)
```
## Executing Scripts
Run the script through ``ConnectionProtocol/execute(sql:)``. Statements execute sequentially in the
order they appear.
```swift
let connection = try Connection(
location: .file(path: dbPath),
options: [.readwrite, .create]
)
try connection.execute(sql: script)
```
In autocommit mode, each statement commits as soon as it succeeds. If any statement fails, execution
stops and previously executed statements remain committed. To ensure all-or-nothing execution, wrap
the script in an explicit transaction:
```swift
try connection.beginTransaction(.immediate)
do {
try connection.execute(sql: script)
try connection.commitTransaction()
} catch {
try? connection.rollbackTransaction()
throw error
}
```
- Important: SQLScript must not include BEGIN, COMMIT, or ROLLBACK. Always manage transactions at
the connection level.
## Executing Raw SQL
Use ``ConnectionProtocol/execute(raw:)`` to run multi-statement SQL directly, without parsing or
preprocessing. This method executes the script exactly as provided, allowing you to manage
transactions explicitly within the SQL itself.
```swift
let migrations = """
BEGIN;
CREATE TABLE categories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
INSERT INTO categories (name) VALUES ('Swift'), ('SQLite');
COMMIT;
"""
try connection.execute(raw: migrations)
```
Each statement runs in sequence until completion or until the first error occurs. If a statement
fails, execution stops and remaining statements are skipped. Open transactions are not rolled back
automatically — they must be handled explicitly inside the script or by the caller.
## Handling Errors
Inspect the thrown ``SQLiteError`` to identify the failing statements result code and message. For
longer scripts, wrap execution in logging to trace progress and isolate the exact statement that
triggered the exception.
- SeeAlso: ``SQLScript``
- SeeAlso: ``ConnectionProtocol/execute(sql:)``
- SeeAlso: ``ConnectionProtocol/execute(raw:)``

View File

@@ -87,8 +87,8 @@ statement in its own transaction.
```swift ```swift
do { do {
try connection.beginTransaction(.immediate) try connection.beginTransaction(.immediate)
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Ada')") try connection.execute(sql: "INSERT INTO users (name) VALUES ('Ada')")
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Grace')") try connection.execute(sql: "INSERT INTO users (name) VALUES ('Grace')")
try connection.commitTransaction() try connection.commitTransaction()
} catch { } catch {
try? connection.rollbackTransaction() try? connection.rollbackTransaction()

View File

@@ -14,7 +14,6 @@ management and SQL execution features with the ergonomics and flexibility of nat
- <doc:CustomFunctions> - <doc:CustomFunctions>
- <doc:PreparedStatements> - <doc:PreparedStatements>
- <doc:SQLiteRows> - <doc:SQLiteRows>
- <doc:SQLScripts>
- <doc:ErrorHandling> - <doc:ErrorHandling>
- <doc:Multithreading> - <doc:Multithreading>
- <doc:DatabaseEncryption> - <doc:DatabaseEncryption>

View File

@@ -1,731 +0,0 @@
import Foundation
extension String {
/// Removes SQL comments from the string while preserving string literals.
///
/// This function preserves escaped single quotes inside string literals
/// and removes both single-line (`-- ...`) and multi-line (`/* ... */`) comments.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func removingComments() -> String {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyoutput = [CChar]()
/// var yycursor = 0
/// loop: while yycursor < yylimit {
/// var llmarker = yycursor/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ([^'] | "''")* "'" {
/// while llmarker < yycursor {
/// yyoutput.append(yyinput[llmarker])
/// llmarker += 1
/// }
/// continue loop }
///
/// "--" [^\r\n\x00]* {
/// continue loop }
///
/// "/*" ([^*] | "*"[^/])* "*/" {
/// continue loop }
///
/// [^] {
/// yyoutput.append(yyinput[llmarker])
/// continue loop }
/// */}
/// yyoutput.append(0)
/// return String(
/// cString: yyoutput,
/// encoding: .utf8
/// ) ?? ""
/// }
/// }
/// ```
@inline(__always)
func removingComments() -> String {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyoutput = [CChar]()
var yycursor = 0
loop: while yycursor < yylimit {
var llmarker = yycursor
var yych: CChar = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 3
continue yyl
case 0x2D:
yystate = 4
continue yyl
case 0x2F:
yystate = 5
continue yyl
default:
yystate = 1
continue yyl
}
case 1:
yystate = 2
continue yyl
case 2:
yyoutput.append(yyinput[llmarker])
continue loop
case 3:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 6
continue yyl
default:
yystate = 3
continue yyl
}
case 4:
yych = yyinput[yycursor]
switch yych {
case 0x2D:
yycursor += 1
yystate = 8
continue yyl
default:
yystate = 2
continue yyl
}
case 5:
yych = yyinput[yycursor]
switch yych {
case 0x2A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 2
continue yyl
}
case 6:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 3
continue yyl
default:
yystate = 7
continue yyl
}
case 7:
while llmarker < yycursor {
yyoutput.append(yyinput[llmarker])
llmarker += 1
}
continue loop
case 8:
yych = yyinput[yycursor]
switch yych {
case 0x00:
fallthrough
case 0x0A:
fallthrough
case 0x0D:
yystate = 9
continue yyl
default:
yycursor += 1
yystate = 8
continue yyl
}
case 9:
continue loop
case 10:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x2A:
yystate = 11
continue yyl
default:
yystate = 10
continue yyl
}
case 11:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x2F:
yystate = 12
continue yyl
default:
yystate = 10
continue yyl
}
case 12:
continue loop
default: fatalError("internal lexer error")
}
}
}
yyoutput.append(0)
return String(cString: yyoutput, encoding: .utf8) ?? ""
}
}
/// Trims empty lines and trailing whitespace outside string literals.
///
/// This function preserves line breaks and whitespace inside string literals,
/// removing only redundant empty lines and trailing whitespace outside literals.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func trimmingLines() -> String {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyoutput = [CChar]()
/// var yycursor = 0
/// var yymarker = 0
/// loop: while yycursor < yylimit {
/// var llmarker = yycursor/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ([^'] | "''")* "'" {
/// while llmarker < yycursor {
/// yyoutput.append(yyinput[llmarker])
/// llmarker += 1
/// }
/// continue loop }
///
/// [ \t]* "\x00" {
/// continue loop }
///
/// [ \t]* "\n\x00" {
/// continue loop }
///
/// [ \t\n]* "\n"+ {
/// if llmarker > 0 && yycursor < yylimit {
/// yyoutput.append(0x0A)
/// }
/// continue loop }
///
/// [^] {
/// yyoutput.append(yyinput[llmarker])
/// continue loop }
/// */}
/// yyoutput.append(0)
/// return String(
/// cString: yyoutput,
/// encoding: .utf8
/// ) ?? ""
/// }
/// }
/// ```
@inline(__always)
func trimmingLines() -> String {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyoutput = [CChar]()
var yycursor = 0
var yymarker = 0
loop: while yycursor < yylimit {
var llmarker = yycursor
var yych: CChar = 0
var yyaccept: UInt = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x00:
yystate = 1
continue yyl
case 0x09:
fallthrough
case 0x20:
yystate = 4
continue yyl
case 0x0A:
yystate = 5
continue yyl
case 0x27:
yystate = 7
continue yyl
default:
yystate = 2
continue yyl
}
case 1:
continue loop
case 2:
yystate = 3
continue yyl
case 3:
yyoutput.append(yyinput[llmarker])
continue loop
case 4:
yyaccept = 0
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x00:
fallthrough
case 0x09...0x0A:
fallthrough
case 0x20:
yystate = 9
continue yyl
default:
yystate = 3
continue yyl
}
case 5:
yyaccept = 1
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x00:
yycursor += 1
yystate = 11
continue yyl
case 0x09...0x0A:
fallthrough
case 0x20:
yystate = 13
continue yyl
default:
yystate = 6
continue yyl
}
case 6:
if llmarker > 0 && yycursor < yylimit {
yyoutput.append(0x0A)
}
continue loop
case 7:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 15
continue yyl
default:
yystate = 7
continue yyl
}
case 8:
yych = yyinput[yycursor]
yystate = 9
continue yyl
case 9:
switch yych {
case 0x00:
yycursor += 1
yystate = 1
continue yyl
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 8
continue yyl
case 0x0A:
yycursor += 1
yystate = 5
continue yyl
default:
yystate = 10
continue yyl
}
case 10:
yycursor = yymarker
if yyaccept == 0 {
yystate = 3
continue yyl
} else {
yystate = 6
continue yyl
}
case 11:
continue loop
case 12:
yych = yyinput[yycursor]
yystate = 13
continue yyl
case 13:
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 12
continue yyl
case 0x0A:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 10
continue yyl
}
case 14:
yyaccept = 1
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 12
continue yyl
case 0x0A:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 6
continue yyl
}
case 15:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 7
continue yyl
default:
yystate = 16
continue yyl
}
case 16:
while llmarker < yycursor {
yyoutput.append(yyinput[llmarker])
llmarker += 1
}
continue loop
default: fatalError("internal lexer error")
}
}
}
yyoutput.append(0)
return String(
cString: yyoutput,
encoding: .utf8
) ?? ""
}
}
/// Splits the SQL script into individual statements by semicolons.
///
/// This function preserves string literals (enclosed in single quotes),
/// and treats `BEGIN...END` blocks as single nested statements, preventing
/// splitting inside these blocks. Statements are split only at semicolons
/// outside string literals and `BEGIN...END` blocks.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func splitStatements() -> [String] {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyranges = [Range<Int>]()
/// var yycursor = 0
/// var yymarker = 0
/// var yynesting = 0
/// var yystart = 0
/// var yyend = 0
/// loop: while yycursor < yylimit {/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ( [^'] | "''" )* "'" {
/// yyend = yycursor
/// continue loop }
///
/// 'BEGIN' {
/// yynesting += 1
/// yyend = yycursor
/// continue loop }
///
/// 'END' {
/// if yynesting > 0 {
/// yynesting -= 1
/// }
/// yyend = yycursor
/// continue loop }
///
/// ";" [ \t]* "\n"* {
/// if yynesting == 0 {
/// if yystart < yyend {
/// yyranges.append(yystart..<yyend)
/// }
/// yystart = yycursor
/// continue loop
/// } else {
/// continue loop
/// }}
///
/// [^] {
/// yyend = yycursor
/// continue loop }
/// */}
/// if yystart < yyend {
/// yyranges.append(yystart..<yyend)
/// }
/// return yyranges.map { range in
/// let buffer = UnsafeBufferPointer<CChar>(
/// start: yyinput.advanced(by: range.lowerBound),
/// count: range.count
/// )
/// let array = Array(buffer) + [0]
/// return String(cString: array, encoding: .utf8) ?? ""
/// }
/// }
/// }
/// ```
@inline(__always)
func splitStatements() -> [String] {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyranges = [Range<Int>]()
var yycursor = 0
var yymarker = 0
var yynesting = 0
var yystart = 0
var yyend = 0
loop: while yycursor < yylimit {
var yych: CChar = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 3
continue yyl
case 0x3B:
yystate = 4
continue yyl
case 0x42:
fallthrough
case 0x62:
yystate = 6
continue yyl
case 0x45:
fallthrough
case 0x65:
yystate = 7
continue yyl
default:
yystate = 1
continue yyl
}
case 1:
yystate = 2
continue yyl
case 2:
yyend = yycursor
continue loop
case 3:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 8
continue yyl
default:
yystate = 3
continue yyl
}
case 4:
yych = yyinput[yycursor]
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 4
continue yyl
case 0x0A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 5
continue yyl
}
case 5:
if yynesting == 0 {
if yystart < yyend {
yyranges.append(yystart..<yyend)
}
yystart = yycursor
continue loop
} else {
continue loop
}
case 6:
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x45:
fallthrough
case 0x65:
yycursor += 1
yystate = 11
continue yyl
default:
yystate = 2
continue yyl
}
case 7:
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x4E:
fallthrough
case 0x6E:
yycursor += 1
yystate = 13
continue yyl
default:
yystate = 2
continue yyl
}
case 8:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 3
continue yyl
default:
yystate = 9
continue yyl
}
case 9:
yyend = yycursor
continue loop
case 10:
yych = yyinput[yycursor]
switch yych {
case 0x0A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 5
continue yyl
}
case 11:
yych = yyinput[yycursor]
switch yych {
case 0x47:
fallthrough
case 0x67:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 12
continue yyl
}
case 12:
yycursor = yymarker
yystate = 2
continue yyl
case 13:
yych = yyinput[yycursor]
switch yych {
case 0x44:
fallthrough
case 0x64:
yycursor += 1
yystate = 15
continue yyl
default:
yystate = 12
continue yyl
}
case 14:
yych = yyinput[yycursor]
switch yych {
case 0x49:
fallthrough
case 0x69:
yycursor += 1
yystate = 16
continue yyl
default:
yystate = 12
continue yyl
}
case 15:
if yynesting > 0 {
yynesting -= 1
}
yyend = yycursor
continue loop
case 16:
yych = yyinput[yycursor]
switch yych {
case 0x4E:
fallthrough
case 0x6E:
yycursor += 1
yystate = 17
continue yyl
default:
yystate = 12
continue yyl
}
case 17:
yynesting += 1
yyend = yycursor
continue loop
default: fatalError("internal lexer error")
}
}
}
if yystart < yyend {
yyranges.append(yystart..<yyend)
}
return yyranges.map { range in
let buffer = UnsafeBufferPointer<CChar>(
start: yyinput.advanced(by: range.lowerBound),
count: range.count
)
let array = Array(buffer) + [0]
return String(cString: array, encoding: .utf8) ?? ""
}
}
}
}

View File

@@ -51,7 +51,6 @@ import Foundation
/// ///
/// ### Executing SQL Commands /// ### Executing SQL Commands
/// ///
/// - ``execute(raw:)``
/// - ``execute(sql:)`` /// - ``execute(sql:)``
/// ///
/// ### Controlling PRAGMA Settings /// ### Controlling PRAGMA Settings
@@ -303,21 +302,11 @@ public protocol ConnectionProtocol: AnyObject {
/// ///
/// Execution stops at the first error, and the corresponding ``SQLiteError`` is thrown. /// Execution stops at the first error, and the corresponding ``SQLiteError`` is thrown.
/// ///
/// - Parameter sql: The SQL text containing one or more statements to execute. /// - Parameter script: The SQL text containing one or more statements to execute.
/// - Throws: ``SQLiteError`` if any statement fails to execute. /// - Throws: ``SQLiteError`` if any statement fails to execute.
/// ///
/// - SeeAlso: [One-Step Query Execution Interface](https://sqlite.org/c3ref/exec.html) /// - SeeAlso: [One-Step Query Execution Interface](https://sqlite.org/c3ref/exec.html)
func execute(raw sql: String) throws(SQLiteError) func execute(sql script: String) throws(SQLiteError)
/// Executes multiple SQL statements from a script.
///
/// The provided ``SQLScript`` may contain one or more SQL statements separated by semicolons.
/// Each statement is executed sequentially using the current connection. This is useful for
/// running migration scripts or initializing database schemas.
///
/// - Parameter script: The SQL script to execute.
/// - Throws: ``SQLiteError`` if any statement in the script fails.
func execute(sql script: SQLScript) throws(SQLiteError)
// MARK: - PRAGMA Control // MARK: - PRAGMA Control
@@ -421,13 +410,6 @@ public extension ConnectionProtocol {
try prepare(sql: query, options: []) try prepare(sql: query, options: [])
} }
func execute(sql script: SQLScript) throws(SQLiteError) {
for query in script {
let stmt = try prepare(sql: query)
while try stmt.step() {}
}
}
func get<T: SQLiteRepresentable>(pragma: Pragma) throws(SQLiteError) -> T? { func get<T: SQLiteRepresentable>(pragma: Pragma) throws(SQLiteError) -> T? {
let stmt = try prepare(sql: "PRAGMA \(pragma)") let stmt = try prepare(sql: "PRAGMA \(pragma)")
switch try stmt.step() { switch try stmt.step() {

View File

@@ -1,276 +0,0 @@
import Foundation
/// A structure representing a collection of SQL queries.
///
/// ## Overview
///
/// `SQLScript` is a structure for loading and processing SQL scripts, representing a collection
/// where each element is an individual SQL query. It allows loading scripts from a file via URL,
/// from the app bundle, or from a string, and provides convenient access to individual SQL queries
/// and the ability to iterate over them.
///
/// ## Usage Examples
///
/// ### Loading from a File
///
/// To load a SQL script from a file in your project, use the following code. In this example, we
/// load a file named `sample_script.sql` from the main app bundle.
///
/// ```swift
/// do {
/// guard let sqlScript = try SQLScript(
/// byResource: "sample_script",
/// extension: "sql"
/// ) else {
/// throw NSError(
/// domain: "SomeDomain",
/// code: -1,
/// userInfo: [:]
/// )
/// }
///
/// for (index, statement) in sqlScript.enumerated() {
/// print("Query \(index + 1):")
/// print(statement)
/// print("--------------------")
/// }
/// } catch {
/// print("Error: \(error.localizedDescription)")
/// }
/// ```
///
/// ### Loading from a String
///
/// If the SQL queries are already contained in a string, you can create an instance of `SQLScript`
/// by passing the string to the initializer. Below is an example where we create a SQL script
/// with two queries: creating a table and inserting data.
///
/// ```swift
/// do {
/// let sqlString = """
/// CREATE TABLE users (
/// id INTEGER PRIMARY KEY,
/// username TEXT NOT NULL,
/// email TEXT NOT NULL
/// );
/// INSERT INTO users (id, username, email)
/// VALUES (1, 'john_doe', 'john@example.com');
/// """
///
/// let sqlScript = try SQLScript(string: sqlString)
///
/// for (index, statement) in sqlScript.enumerated() {
/// print("Query \(index + 1):")
/// print(statement)
/// print("--------------------")
/// }
/// } catch {
/// print("Error: \(error.localizedDescription)")
/// }
/// ```
///
/// ## SQL Format and Syntax
///
/// `SQLScript` is designed to handle SQL scripts that contain one or more SQL queries. Each query
/// must end with a semicolon (`;`), which indicates the end of the statement.
///
/// **Supported Features:**
///
/// - **Command Separation:** Each query ends with a semicolon (`;`), marking the end of the command.
/// - **Formatting:** SQLScript removes lines containing only whitespace or comments, keeping
/// only the valid SQL queries in the collection.
/// - **Comment Support:** Single-line (`--`) and multi-line (`/* */`) comments are supported.
///
/// **Example of a correctly formatted SQL script:**
///
/// ```sql
/// -- Create users table
/// CREATE TABLE users (
/// id INTEGER PRIMARY KEY,
/// username TEXT NOT NULL,
/// email TEXT NOT NULL
/// );
///
/// -- Insert data into users table
/// INSERT INTO users (id, username, email)
/// VALUES (1, 'john_doe', 'john@example.com');
///
/// /* Update user data */
/// UPDATE users
/// SET email = 'john.doe@example.com'
/// WHERE id = 1;
/// ```
///
/// - Important: Nested comments are not supported, so avoid placing multi-line comments inside
/// other multi-line comments.
///
/// - Important: `SQLScript` does not support SQL scripts containing transactions. To execute an
/// `SQLScript`, use the method ``ConnectionProtocol/execute(sql:)``, which executes each statement
/// individually in autocommit mode.
///
/// If you need to execute the entire `SQLScript` within a single transaction, use the methods
/// ``ConnectionProtocol/beginTransaction(_:)``, ``ConnectionProtocol/commitTransaction()``, and
/// ``ConnectionProtocol/rollbackTransaction()`` to manage the transaction explicitly.
///
/// If your SQL script includes transaction statements (e.g., BEGIN, COMMIT, ROLLBACK), execute
/// the entire script using ``ConnectionProtocol/execute(raw:)``.
///
/// - Important: This class is not designed to work with untrusted user data. Never insert
/// user-provided data directly into SQL queries without proper sanitization or parameterization.
/// Unfiltered data can lead to SQL injection attacks, which pose a security risk to your data and
/// database. For more information about SQL injection risks, see the OWASP documentation:
/// [SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection).
public struct SQLScript: Collection, ExpressibleByStringLiteral {
/// The type representing the index in the collection of SQL queries.
///
/// This type is used to access elements in the `SQLScript` collection. The index is an
/// integer (`Int`) indicating the position of the SQL query in the collection.
public typealias Index = Int
/// The type representing an element in the collection of SQL queries.
///
/// This type defines that each element in the `SQLScript` collection is a string (`String`).
/// Each element represents a separate SQL query that can be loaded and processed.
public typealias Element = String
// MARK: - Properties
/// An array containing SQL queries.
private var elements: [Element]
/// The starting index of the collection of SQL queries.
///
/// This property returns the index of the first element in the collection. The starting
/// index is used to initialize iterators and access the elements of the collection. If
/// the collection is empty, this value will equal `endIndex`.
public var startIndex: Index {
elements.startIndex
}
/// The end index of the collection of SQL queries.
///
/// This property returns the index that indicates the position following the last element
/// of the collection. The end index is used for iterating over the collection and marks
/// the point where the collection ends. If the collection is empty, this value will equal
/// `startIndex`.
public var endIndex: Index {
elements.endIndex
}
/// The number of SQL queries in the collection.
///
/// This property returns the total number of SQL queries stored in the collection. The
/// value will be 0 if the collection is empty. Use this property to know how many queries
/// can be processed or iterated over.
public var count: Int {
elements.count
}
/// A Boolean value indicating whether the collection is empty.
///
/// This property returns `true` if there are no SQL queries in the collection, and `false`
/// otherwise. Use this property to check for the presence of SQL queries before performing
/// operations that require elements in the collection.
public var isEmpty: Bool {
elements.isEmpty
}
// MARK: - Inits
/// Initializes an instance of `SQLScript`, loading SQL queries from a resource file.
///
/// This initializer looks for a file with the specified name and extension in the given
/// bundle and loads its contents as SQL queries.
///
/// - If `name` is `nil`, the first found resource file with the specified extension will
/// be loaded.
/// - If `extension` is an empty string or `nil`, it is assumed that the extension does
/// not exist, and the first found file that exactly matches the name will be loaded.
/// - Returns `nil` if the specified file is not found.
///
/// - Parameters:
/// - name: The name of the resource file containing SQL queries.
/// - extension: The extension of the resource file. Defaults to `nil`.
/// - bundle: The bundle from which to load the resource file. Defaults to `.main`.
///
/// - Throws: An error if the file cannot be loaded or processed.
public init?(
byResource name: String?,
extension: String? = nil,
in bundle: Bundle = .main
) throws {
guard
let url = bundle.url(
forResource: name,
withExtension: `extension`
)
else { return nil }
try self.init(contentsOf: url)
}
/// Initializes an instance of `SQLScript`, loading SQL queries from the specified file.
///
/// This initializer takes a URL to a file and loads its contents as SQL queries.
///
/// - Parameter url: The URL of the file containing SQL queries.
///
/// - Throws: An error if the file cannot be loaded or processed.
public init(contentsOf url: URL) throws {
try self.init(string: .init(contentsOf: url, encoding: .utf8))
}
/// Initializes an instance of `SQLScript` from a string literal.
///
/// This initializer allows you to create a `SQLScript` instance directly from a string literal.
/// The string is parsed into individual SQL queries, removing comments and empty lines.
///
/// - Parameter value: The string literal containing SQL queries. Each query should be separated
/// by semicolons (`;`), and the string can include comments and empty lines, which will
/// be ignored during initialization.
///
/// - Warning: The string literal should represent valid SQL queries. Invalid syntax or
/// improperly formatted SQL may lead to an error at runtime.
public init(stringLiteral value: StringLiteralType) {
self.init(string: value)
}
/// Initializes an instance of `SQLScript` by parsing SQL queries from the specified string.
///
/// This initializer takes a string containing SQL queries and extracts individual queries,
/// removing comments and empty lines.
///
/// - Parameter string: The string containing SQL queries.
public init(string: String) {
elements =
string
.removingComments()
.trimmingLines()
.splitStatements()
}
// MARK: - Collection Methods
/// Accesses the element at the specified position in the collection.
///
/// - Parameter index: The index of the SQL query in the collection. The index must be within the
/// bounds of the collection. If an out-of-bounds index is provided, a runtime error will occur.
///
/// - Returns: The SQL query as a string at the specified index.
public subscript(index: Index) -> Element {
elements[index]
}
/// Returns the index after the given index.
///
/// This method is used for iterating through the collection. It provides the next valid
/// index following the specified index.
///
/// - Parameter i: The index of the current element.
///
/// - Returns: The index of the next element in the collection. If `i` is the last
/// index in the collection, this method returns `endIndex`, which is one past the
/// last valid index.
public func index(after i: Index) -> Index {
elements.index(after: i)
}
}

View File

@@ -91,12 +91,12 @@ struct ConnectionTests {
options: [.create, .readwrite, .fullmutex] options: [.create, .readwrite, .fullmutex]
) )
try oneConn.execute(raw: """ try oneConn.execute(sql: """
CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT); CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);
""") """)
try oneConn.beginTransaction() try oneConn.beginTransaction()
try oneConn.execute(raw: """ try oneConn.execute(sql: """
INSERT INTO test (value) VALUES ('first'); INSERT INTO test (value) VALUES ('first');
""") """)
@@ -107,7 +107,7 @@ struct ConnectionTests {
), ),
performing: { performing: {
twoConn.busyTimeout = 0 twoConn.busyTimeout = 0
try twoConn.execute(raw: """ try twoConn.execute(sql: """
INSERT INTO test (value) VALUES ('second'); INSERT INTO test (value) VALUES ('second');
""") """)
} }
@@ -207,7 +207,7 @@ struct ConnectionTests {
INSERT INTO items (value) VALUES (1), (2), (NULL), (3); INSERT INTO items (value) VALUES (1), (2), (NULL), (3);
""") """)
try connection.add(function: function) try connection.add(function: function)
try connection.execute(raw: "SELECT \(name)(value) FROM items") try connection.execute(sql: "SELECT \(name)(value) FROM items")
} }
@Test(arguments: [ @Test(arguments: [
@@ -234,7 +234,7 @@ struct ConnectionTests {
message: "no such function: \(name)" message: "no such function: \(name)"
), ),
performing: { performing: {
try connection.execute(raw: """ try connection.execute(sql: """
SELECT \(name)(value) FROM items SELECT \(name)(value) FROM items
""") """)
} }

View File

@@ -1,394 +0,0 @@
import Foundation
import Testing
@testable import DataLiteCore
struct StringSQLTests {
// MARK: - Test Remove Single Line Comments
@Test func testSingleLineCommentAtStart() {
let input = """
-- This is a comment
SELECT * FROM users;
"""
let expected = """
SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testSingleLineCommentAfterStatement() {
let input = """
SELECT * FROM users; -- This is a comment
"""
let expected = """
SELECT * FROM users;\u{0020}
"""
#expect(input.removingComments() == expected)
}
@Test func testSingleLineCommentBetweenStatementLines() {
let input = """
INSERT INTO users (
id, name
-- comment between statement
) VALUES (1, 'Alice');
"""
let expected = """
INSERT INTO users (
id, name
) VALUES (1, 'Alice');
"""
#expect(input.removingComments() == expected)
}
@Test func testSingleLineCommentAtEnd() {
let input = """
SELECT * FROM users;
-- final comment
"""
let expected = """
SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testSingleLineCommentWithTabsAndSpaces() {
let input = "SELECT 1;\t -- comment with tab\nSELECT 2;"
let expected = "SELECT 1;\t \nSELECT 2;"
#expect(input.removingComments() == expected)
}
@Test func testSingleLineCommentWithLiterals() {
let input = """
INSERT INTO logs (text) VALUES ('This isn''t -- a comment'); -- trailing comment
"""
let expected = """
INSERT INTO logs (text) VALUES ('This isn''t -- a comment');
"""
#expect(input.removingComments() == expected)
}
// MARK: - Test Remove Multiline Comments
@Test func testMultilineCommentAtStart() {
let input = """
/* This is a
comment at the top */
SELECT * FROM users;
"""
let expected = """
SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentAtLineStart() {
let input = """
/* comment */ SELECT * FROM users;
"""
let expected = """
\u{0020}SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentInMiddleOfLine() {
let input = """
SELECT /* inline comment */ * FROM users;
"""
let expected = """
SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentAtEndOfLine() {
let input = """
SELECT * FROM users; /* trailing comment */
"""
let expected = """
SELECT * FROM users;\u{0020}
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentBetweenLines() {
let input = """
INSERT INTO users (
id,
/* this field stores username */
username,
email
) VALUES (1, 'alice', 'alice@example.com');
"""
let expected = """
INSERT INTO users (
id,
username,
email
) VALUES (1, 'alice', 'alice@example.com');
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentAtEndOfFile() {
let input = """
SELECT * FROM users;
/* final block comment */
"""
let expected = """
SELECT * FROM users;
"""
#expect(input.removingComments() == expected)
}
@Test func testMultilineCommentWithLiterals() {
let input = """
INSERT INTO notes (text) VALUES ('This isn''t /* a comment */ either'); /* trailing comment */
"""
let expected = """
INSERT INTO notes (text) VALUES ('This isn''t /* a comment */ either');\u{0020}
"""
#expect(input.removingComments() == expected)
}
// MARK: - Test Trimming Lines
@Test func testTrimmingEmptyFirstLine() {
let input = "\nSELECT * FROM users;"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyFirstLineWithSpace() {
let input = " \nSELECT * FROM users;"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyFirstLineWithTab() {
let input = "\t\nSELECT * FROM users;"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyMiddleLine() {
let input = "SELECT *\n\nFROM users;"
let expected = "SELECT *\nFROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyMiddleLineWithSpace() {
let input = "SELECT *\n\u{0020}\nFROM users;"
let expected = "SELECT *\nFROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyMiddleLineWithTab() {
let input = "SELECT *\n\t\nFROM users;"
let expected = "SELECT *\nFROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyLastLine() {
let input = "SELECT * FROM users;\n"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyLastLineWithSpace() {
let input = "SELECT * FROM users; \n"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingEmptyLastLineWithTab() {
let input = "SELECT * FROM users;\t\n"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingTrailingSpacesOnly() {
let input = "SELECT * FROM users; "
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingTrailingSpacesAndNewline() {
let input = "SELECT * FROM users; \n"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingTrailingTabsOnly() {
let input = "SELECT * FROM users;\t\t"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingTrailingTabsAndNewline() {
let input = "SELECT * FROM users;\t\t\n"
let expected = "SELECT * FROM users;"
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingMultipleEmptyLinesAndSpaces() {
let input = "\n\n\t\u{0020}\nSELECT * FROM users;\n\n\u{0020}\n\n"
let expected = "SELECT * FROM users;"
print("zzzz\n\(input.trimmingLines())")
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingLiteralPreservesWhitespace() {
let input = "INSERT INTO logs VALUES ('line with\n\nspaces \t \n\n and tabs');"
let expected = input
#expect(input.trimmingLines() == expected)
}
@Test func testTrimmingPreserveLineBreaksInMultilineInsert() {
let input = """
INSERT INTO users (id, username, email)
VALUES \t
(1, 'john_doe', 'john@example.com'),
(2, 'jane_doe', 'jane@example.com');
"""
let expected = """
INSERT INTO users (id, username, email)
VALUES
(1, 'john_doe', 'john@example.com'),
(2, 'jane_doe', 'jane@example.com');
"""
#expect(input.trimmingLines() == expected)
}
// MARK: - Test Split Statements
@Test func testSplitSingleStatement() {
let input = "SELECT * FROM users;"
let expected = ["SELECT * FROM users"]
#expect(input.splitStatements() == expected)
}
@Test func testSplitSingleStatementWithoutSemicolon() {
let input = "SELECT * FROM users"
let expected = ["SELECT * FROM users"]
#expect(input.splitStatements() == expected)
}
@Test func testSplitMultipleStatements() {
let input = """
SELECT * FROM users;
DELETE FROM users WHERE id=123;
DELETE FROM users WHERE id=987;
"""
let expected = [
"SELECT * FROM users",
"DELETE FROM users WHERE id=123",
"DELETE FROM users WHERE id=987"
]
#expect(input.splitStatements() == expected)
}
@Test func testSplitMultipleStatementsLastWithoutSemicolon() {
let input = """
SELECT * FROM users;
DELETE FROM users WHERE id=1;
UPDATE users SET name='Bob' WHERE id=2
"""
let expected = [
"SELECT * FROM users",
"DELETE FROM users WHERE id=1",
"UPDATE users SET name='Bob' WHERE id=2"
]
#expect(input.splitStatements() == expected)
}
@Test func testSplitTextLiteralSemicolon() {
let input = "INSERT INTO logs (msg) VALUES ('Hello; world');"
let expected = ["INSERT INTO logs (msg) VALUES ('Hello; world')"]
#expect(input.splitStatements() == expected)
}
@Test func testSplitTextLiteralEscapingQuotes() {
let input = "INSERT INTO test VALUES ('It''s a test');"
let expected = ["INSERT INTO test VALUES ('It''s a test')"]
#expect(input.splitStatements() == expected)
}
@Test func testSplitMultipleSemicolon() {
let input = "SELECT * FROM users;;SELECT * FROM users;"
let expected = [
"SELECT * FROM users",
"SELECT * FROM users"
]
#expect(input.splitStatements() == expected)
}
@Test(arguments: [
("BEGIN", "END"),
("Begin", "End"),
("begin", "end"),
("bEgIn", "eNd"),
("BeGiN", "EnD")
])
func testSplitWithBeginEnd(begin: String, end: String) {
let input = """
CREATE TABLE KDFMetadata (
id INTEGER PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TRIGGER KDFMetadataLimit
BEFORE INSERT ON KDFMetadata
WHEN (SELECT COUNT(*) FROM KDFMetadata) >= 1
\(begin)
SELECT RAISE(FAIL, 'Only one row allowed in KDFMetadata');
\(end);
"""
let expected = [
"""
CREATE TABLE KDFMetadata (
id INTEGER PRIMARY KEY,
value TEXT NOT NULL
)
""",
"""
CREATE TRIGGER KDFMetadataLimit
BEFORE INSERT ON KDFMetadata
WHEN (SELECT COUNT(*) FROM KDFMetadata) >= 1
\(begin)
SELECT RAISE(FAIL, 'Only one row allowed in KDFMetadata');
\(end)
"""
]
#expect(input.splitStatements() == expected)
}
@Test func testSplitWithSavepoints() {
let input = """
SAVEPOINT sp1;
INSERT INTO users (id, name) VALUES (1, 'Alice');
RELEASE SAVEPOINT sp1;
SAVEPOINT sp2;
UPDATE users SET name = 'Bob' WHERE id = 1;
ROLLBACK TO SAVEPOINT sp2;
RELEASE SAVEPOINT sp2;
"""
let expected = [
"SAVEPOINT sp1",
"INSERT INTO users (id, name) VALUES (1, 'Alice')",
"RELEASE SAVEPOINT sp1",
"SAVEPOINT sp2",
"UPDATE users SET name = 'Bob' WHERE id = 1",
"ROLLBACK TO SAVEPOINT sp2",
"RELEASE SAVEPOINT sp2"
]
#expect(input.splitStatements() == expected)
}
}

View File

@@ -1,64 +0,0 @@
import XCTest
import DataLiteCore
class SQLScriptTests: XCTestCase {
func testInitWithValidFile() throws {
let sqlScript = try SQLScript(
byResource: "valid_script",
extension: "sql",
in: .module
)
XCTAssertNotNil(sqlScript)
XCTAssertEqual(sqlScript?.count, 2)
}
func testInitWithEmptyFile() throws {
let sqlScript = try SQLScript(
byResource: "empty_script",
extension: "sql",
in: .module
)
XCTAssertNotNil(sqlScript)
XCTAssertEqual(sqlScript?.count, 0)
}
func testInitWithInvalidFile() throws {
XCTAssertThrowsError(
try SQLScript(
byResource: "invalid_script",
extension: "sql",
in: .module
)
) { error in
let error = error as NSError
XCTAssertEqual(error.domain, NSCocoaErrorDomain)
XCTAssertEqual(error.code, 259)
}
}
func testAccessingStatements() throws {
let sqlScript = try SQLScript(
byResource: "valid_script",
extension: "sql",
in: .module
)
let oneStatement = """
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL
)
"""
let twoStatement = """
INSERT INTO users (id, username, email)
VALUES
(1, 'john_doe', 'john@example.com'),
(2, 'jane_doe', 'jane@example.com')
"""
XCTAssertEqual(sqlScript?[0], oneStatement)
XCTAssertEqual(sqlScript?[1], twoStatement)
}
}