Remove SQLScript

This commit is contained in:
2025-10-25 18:33:03 +03:00
parent 954062f6c6
commit 835f7ee380
13 changed files with 16 additions and 1598 deletions

View File

@@ -203,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) }
}
}

View File

@@ -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.*';
/// """)
/// ```

View File

@@ -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")
```

View File

@@ -30,7 +30,7 @@ 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 {
@@ -47,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.

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
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()

View File

@@ -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>

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
///
/// - ``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() {

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]
)
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
""")
}

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)
}
}