274 lines
11 KiB
Swift
274 lines
11 KiB
Swift
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 ``Connection/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
|
|
/// ``Connection/beginTransaction(_:)``, ``Connection/commitTransaction()``, and
|
|
/// ``Connection/rollbackTransaction()`` to manage the transaction explicitly.
|
|
///
|
|
/// If your SQL script includes transaction statements (e.g., BEGIN, COMMIT, ROLLBACK),
|
|
/// execute the entire script using ``Connection/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)
|
|
}
|
|
}
|