From 835f7ee38091ea30eacc3650d211b54beaf27827 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sat, 25 Oct 2025 18:33:03 +0300 Subject: [PATCH] Remove SQLScript --- Sources/DataLiteCore/Classes/Connection.swift | 4 +- .../Classes/Function+Regexp.swift | 2 +- .../Docs.docc/Articles/DatabaseEncryption.md | 2 +- .../Docs.docc/Articles/ErrorHandling.md | 6 +- .../Docs.docc/Articles/SQLScripts.md | 98 --- .../Articles/WorkingWithConnections.md | 4 +- .../DataLiteCore/Docs.docc/DataLiteCore.md | 1 - .../DataLiteCore/Extensions/String+SQL.swift | 731 ------------------ .../Protocols/ConnectionProtocol.swift | 22 +- .../DataLiteCore/Structures/SQLScript.swift | 276 ------- .../Classes/ConnectionTests.swift | 10 +- .../Extensions/String+SQLTests.swift | 394 ---------- .../Structures/SQLScriptTests.swift | 64 -- 13 files changed, 16 insertions(+), 1598 deletions(-) delete mode 100644 Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md delete mode 100644 Sources/DataLiteCore/Extensions/String+SQL.swift delete mode 100644 Sources/DataLiteCore/Structures/SQLScript.swift delete mode 100644 Tests/DataLiteCoreTests/Extensions/String+SQLTests.swift delete mode 100644 Tests/DataLiteCoreTests/Structures/SQLScriptTests.swift diff --git a/Sources/DataLiteCore/Classes/Connection.swift b/Sources/DataLiteCore/Classes/Connection.swift index fdfe41b..ce5c129 100644 --- a/Sources/DataLiteCore/Classes/Connection.swift +++ b/Sources/DataLiteCore/Classes/Connection.swift @@ -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) } } } diff --git a/Sources/DataLiteCore/Classes/Function+Regexp.swift b/Sources/DataLiteCore/Classes/Function+Regexp.swift index cec89fd..0db0100 100644 --- a/Sources/DataLiteCore/Classes/Function+Regexp.swift +++ b/Sources/DataLiteCore/Classes/Function+Regexp.swift @@ -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.*'; /// """) /// ``` diff --git a/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md b/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md index f9bf5fe..f41ff09 100644 --- a/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md +++ b/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md @@ -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") ``` diff --git a/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md b/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md index 9ef2338..84c4ee7 100644 --- a/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md +++ b/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md @@ -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. diff --git a/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md b/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md deleted file mode 100644 index 11cd858..0000000 --- a/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md +++ /dev/null @@ -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:)`` diff --git a/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md b/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md index 2d84a01..630fef9 100644 --- a/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md +++ b/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md @@ -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() diff --git a/Sources/DataLiteCore/Docs.docc/DataLiteCore.md b/Sources/DataLiteCore/Docs.docc/DataLiteCore.md index 50a453d..712947c 100644 --- a/Sources/DataLiteCore/Docs.docc/DataLiteCore.md +++ b/Sources/DataLiteCore/Docs.docc/DataLiteCore.md @@ -14,7 +14,6 @@ management and SQL execution features with the ergonomics and flexibility of nat - - - -- - - - diff --git a/Sources/DataLiteCore/Extensions/String+SQL.swift b/Sources/DataLiteCore/Extensions/String+SQL.swift deleted file mode 100644 index 3bf28f9..0000000 --- a/Sources/DataLiteCore/Extensions/String+SQL.swift +++ /dev/null @@ -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]() - /// 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..( - /// 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]() - 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.. 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..( - start: yyinput.advanced(by: range.lowerBound), - count: range.count - ) - let array = Array(buffer) + [0] - return String(cString: array, encoding: .utf8) ?? "" - } - } - } -} diff --git a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift index 8ddd415..c83d3eb 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift @@ -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(pragma: Pragma) throws(SQLiteError) -> T? { let stmt = try prepare(sql: "PRAGMA \(pragma)") switch try stmt.step() { diff --git a/Sources/DataLiteCore/Structures/SQLScript.swift b/Sources/DataLiteCore/Structures/SQLScript.swift deleted file mode 100644 index d2b46e8..0000000 --- a/Sources/DataLiteCore/Structures/SQLScript.swift +++ /dev/null @@ -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) - } -} diff --git a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift index 83b0074..222d21a 100644 --- a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift +++ b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift @@ -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 """) } diff --git a/Tests/DataLiteCoreTests/Extensions/String+SQLTests.swift b/Tests/DataLiteCoreTests/Extensions/String+SQLTests.swift deleted file mode 100644 index 76f5838..0000000 --- a/Tests/DataLiteCoreTests/Extensions/String+SQLTests.swift +++ /dev/null @@ -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) - } -} diff --git a/Tests/DataLiteCoreTests/Structures/SQLScriptTests.swift b/Tests/DataLiteCoreTests/Structures/SQLScriptTests.swift deleted file mode 100644 index c251628..0000000 --- a/Tests/DataLiteCoreTests/Structures/SQLScriptTests.swift +++ /dev/null @@ -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) - } -}