Merge branch 'feature/refactoring' into develop
This commit is contained in:
@@ -48,8 +48,6 @@ public final class Connection {
|
||||
/// Initializes a new connection to an SQLite database.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
@@ -73,15 +71,7 @@ public final class Connection {
|
||||
///
|
||||
/// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to
|
||||
/// 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 {
|
||||
if case let Location.file(path) = location, !path.isEmpty {
|
||||
try FileManager.default.createDirectory(
|
||||
at: URL(fileURLWithPath: path).deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
}
|
||||
|
||||
public init(location: Location, options: Options) throws(SQLiteError) {
|
||||
var connection: OpaquePointer! = 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,
|
||||
/// 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 {
|
||||
public convenience init(path: String, options: Options) throws(SQLiteError) {
|
||||
try self.init(location: .file(path: path), options: options)
|
||||
}
|
||||
|
||||
@@ -214,8 +203,8 @@ extension Connection: ConnectionProtocol {
|
||||
try Statement(db: connection, sql: query, options: options)
|
||||
}
|
||||
|
||||
public func execute(raw sql: String) throws(SQLiteError) {
|
||||
let status = sqlite3_exec(connection, sql, nil, nil, nil)
|
||||
public func execute(sql script: String) throws(SQLiteError) {
|
||||
let status = sqlite3_exec(connection, script, nil, nil, nil)
|
||||
guard status == SQLITE_OK else { throw SQLiteError(connection) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension Function {
|
||||
/// )
|
||||
/// try connection.add(function: Function.Regexp.self)
|
||||
///
|
||||
/// try connection.execute(raw: """
|
||||
/// try connection.execute(sql: """
|
||||
/// SELECT * FROM users WHERE name REGEXP 'John.*';
|
||||
/// """)
|
||||
/// ```
|
||||
|
||||
@@ -49,7 +49,7 @@ Use `nil` or `"main"` for the primary database, `"temp"` for the temporary one,
|
||||
others.
|
||||
|
||||
```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")
|
||||
```
|
||||
|
||||
|
||||
@@ -25,13 +25,12 @@ SQLite messages verbatim.
|
||||
Most DataLiteCore APIs are annotated as `throws(SQLiteError)`, meaning they only throw SQLiteError
|
||||
instances.
|
||||
|
||||
Only APIs that touch the file system or execute arbitrary user code may throw other error
|
||||
types — for example, ``Connection/init(location:options:)`` when creating directories for on-disk
|
||||
databases.
|
||||
Only APIs that execute arbitrary user code or integrate with external systems may surface other
|
||||
error types. Consult the documentation on each API for specific details.
|
||||
|
||||
```swift
|
||||
do {
|
||||
try connection.execute(raw: """
|
||||
try connection.execute(sql: """
|
||||
INSERT INTO users(email) VALUES ('ada@example.com')
|
||||
""")
|
||||
} catch {
|
||||
@@ -48,8 +47,8 @@ do {
|
||||
|
||||
## Multi-Statement Scenarios
|
||||
|
||||
- ``ConnectionProtocol/execute(sql:)`` and ``ConnectionProtocol/execute(raw:)`` stop at the first
|
||||
failing statement and propagate its ``SQLiteError``.
|
||||
- ``ConnectionProtocol/execute(sql:)`` stops at the first failing statement and propagates its
|
||||
``SQLiteError``.
|
||||
- ``StatementProtocol/execute(_:)`` reuses prepared statements; inside `catch` blocks, remember to
|
||||
call ``StatementProtocol/reset()`` and (if needed) ``StatementProtocol/clearBindings()`` before
|
||||
retrying.
|
||||
|
||||
@@ -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 statement’s 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:)``
|
||||
@@ -87,8 +87,8 @@ statement in its own transaction.
|
||||
```swift
|
||||
do {
|
||||
try connection.beginTransaction(.immediate)
|
||||
try connection.execute(raw: "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 ('Ada')")
|
||||
try connection.execute(sql: "INSERT INTO users (name) VALUES ('Grace')")
|
||||
try connection.commitTransaction()
|
||||
} catch {
|
||||
try? connection.rollbackTransaction()
|
||||
|
||||
@@ -14,7 +14,6 @@ management and SQL execution features with the ergonomics and flexibility of nat
|
||||
- <doc:CustomFunctions>
|
||||
- <doc:PreparedStatements>
|
||||
- <doc:SQLiteRows>
|
||||
- <doc:SQLScripts>
|
||||
- <doc:ErrorHandling>
|
||||
- <doc:Multithreading>
|
||||
- <doc:DatabaseEncryption>
|
||||
|
||||
@@ -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) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@ import Foundation
|
||||
///
|
||||
/// ### Executing SQL Commands
|
||||
///
|
||||
/// - ``execute(raw:)``
|
||||
/// - ``execute(sql:)``
|
||||
///
|
||||
/// ### Controlling PRAGMA Settings
|
||||
@@ -303,21 +302,11 @@ public protocol ConnectionProtocol: AnyObject {
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// - SeeAlso: [One-Step Query Execution Interface](https://sqlite.org/c3ref/exec.html)
|
||||
func execute(raw sql: 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)
|
||||
func execute(sql script: String) throws(SQLiteError)
|
||||
|
||||
// MARK: - PRAGMA Control
|
||||
|
||||
@@ -421,13 +410,6 @@ public extension ConnectionProtocol {
|
||||
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? {
|
||||
let stmt = try prepare(sql: "PRAGMA \(pragma)")
|
||||
switch try stmt.step() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,12 @@ struct ConnectionTests {
|
||||
options: [.create, .readwrite, .fullmutex]
|
||||
)
|
||||
|
||||
try oneConn.execute(raw: """
|
||||
try oneConn.execute(sql: """
|
||||
CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);
|
||||
""")
|
||||
|
||||
try oneConn.beginTransaction()
|
||||
try oneConn.execute(raw: """
|
||||
try oneConn.execute(sql: """
|
||||
INSERT INTO test (value) VALUES ('first');
|
||||
""")
|
||||
|
||||
@@ -107,7 +107,7 @@ struct ConnectionTests {
|
||||
),
|
||||
performing: {
|
||||
twoConn.busyTimeout = 0
|
||||
try twoConn.execute(raw: """
|
||||
try twoConn.execute(sql: """
|
||||
INSERT INTO test (value) VALUES ('second');
|
||||
""")
|
||||
}
|
||||
@@ -207,7 +207,7 @@ struct ConnectionTests {
|
||||
INSERT INTO items (value) VALUES (1), (2), (NULL), (3);
|
||||
""")
|
||||
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: [
|
||||
@@ -234,7 +234,7 @@ struct ConnectionTests {
|
||||
message: "no such function: \(name)"
|
||||
),
|
||||
performing: {
|
||||
try connection.execute(raw: """
|
||||
try connection.execute(sql: """
|
||||
SELECT \(name)(value) FROM items
|
||||
""")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user