├── _config.yml ├── SundeedLogo.png ├── codecov.yml ├── SundeedQLiteLibrary ├── Assets.xcassets │ ├── Contents.json │ ├── 1.imageset │ │ ├── 1.png │ │ └── Contents.json │ ├── 2.imageset │ │ ├── 2.png │ │ └── Contents.json │ ├── 3.imageset │ │ ├── 3.png │ │ └── Contents.json │ ├── 4.imageset │ │ ├── 4.png │ │ └── Contents.json │ ├── 5.imageset │ │ ├── 5.png │ │ └── Contents.json │ ├── image.imageset │ │ ├── SundeedLogoBlack.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift └── Info.plist ├── SundeedQLiteLibrary.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── xcbaselines │ └── 06F8A1812349084B001D1381.xcbaseline │ │ ├── 6FF2C487-D033-4241-8C96-0FC8B5152E67.plist │ │ ├── 1C3A1CCB-CE1E-4D96-BC40-278674C2DA72.plist │ │ └── Info.plist │ └── xcschemes │ └── SundeedQLiteLibrary.xcscheme ├── Library ├── ClassHandler │ ├── SundeedQLiteConverter.swift │ ├── Map │ │ ├── SundeedQLiteMap+Primary.swift │ │ ├── SundeedQLiteMap+Ordering.swift │ │ ├── SundeedQLiteMap.swift │ │ └── SundeedQLiteMap+ArrayMandatory.swift │ └── SundeedQLiter.swift └── Main │ ├── Processor │ ├── Processors.swift │ ├── ObjectWrapper.swift │ ├── Processor.swift │ └── Processors │ │ ├── CreateTableProcessor.swift │ │ ├── RetrieveProcessor.swift │ │ └── UpdateProcessor.swift │ ├── Statements │ ├── StatementBuilder.swift │ ├── Statement.swift │ ├── DeleteStatement.swift │ ├── CreateTableStatement.swift │ ├── InsertStatement.swift │ ├── UpdateStatement.swift │ └── SelectStatement.swift │ ├── SundeedLog.swift │ ├── Sundeed.swift │ ├── SundeedQLiteError.swift │ ├── SundeedQLiterLibrary.swift │ ├── SundeedExpression.swift │ └── SundeedQLiteConnection.swift ├── Package.swift ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── test.yml ├── SundeedQLiteLibraryTests ├── Statements │ ├── SelectStatementTests.swift │ └── StatementHelpersTests.swift ├── Info.plist ├── Mandatory Array Operator │ ├── ArrayMandatoryTestWithEmpty.swift │ ├── ArrayMandatoryTestWithNil.swift │ ├── ArrayMandatoryClasses.swift │ └── ArrayMandatoryTestWithData.swift ├── Operations │ ├── RetrieveWithLimitAndSkip.swift │ ├── RetrieveWithSorting.swift │ └── OperationTests.swift ├── SundeedLog │ └── SundeedLog.swift ├── No Primary Keys │ ├── OperationTestsWithoutPrimaryKey.swift │ └── NoPrimariesClasses.swift ├── ErrorCodes │ └── SundeedQLiteErrorTests.swift ├── Updates │ └── UpdateTests.swift ├── Filters │ └── SundeedExpressionTests.swift ├── Listeners │ └── Listeners.swift ├── Mandatory Operator │ ├── MandatoryClasses.swift │ └── MandatoryOperatorTestWithoutData.swift └── Main │ └── SundeedQLiteLibraryTests.swift ├── license ├── .gitignore └── CODE_OF_CONDUCT.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /SundeedLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedLogo.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 95% 6 | threshold: 1% 7 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/1.imageset/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/1.imageset/1.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/2.imageset/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/2.imageset/2.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/3.imageset/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/3.imageset/3.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/4.imageset/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/4.imageset/4.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/5.imageset/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/5.imageset/5.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/image.imageset/SundeedLogoBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noursandid/SundeedQLite/HEAD/SundeedQLiteLibrary/Assets.xcassets/image.imageset/SundeedLogoBlack.png -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "2.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "3.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "4.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "5.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "SundeedLogoBlack.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SundeedQLiteLibrary/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 10/5/19. 6 | // Copyright © 2019 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate {} 13 | 14 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Library/ClassHandler/SundeedQLiteConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteConverter.swift 3 | // AppointSync 4 | // 5 | // Created by Nour Sandid on 1/9/19. 6 | // Copyright © 2019 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SundeedQLiteConverter: AnyObject { 12 | func toString(value: Any?) -> String? 13 | func fromString(value: String) -> Any? 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SundeedQLite", 6 | platforms: [ 7 | .iOS(.v13) 8 | ], 9 | products: [ 10 | .library( 11 | name: "SundeedQLite", 12 | targets: ["SundeedQLite"] 13 | ), 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "SundeedQLite", 19 | path: "Library" 20 | ) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Library/ClassHandler/Map/SundeedQLiteMap+Primary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteMap+Primary.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/16/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | postfix operator + 12 | public postfix func + (left: SundeedQLiteMap) -> SundeedQLiteMap { 13 | assert(!left.hasPrimaryKey, "You can only have one primary key") 14 | guard let key = left.key else { 15 | fatalError("You can only have one primary key") 16 | } 17 | left.primaryKey = key 18 | left.hasPrimaryKey = true 19 | return left 20 | } 21 | -------------------------------------------------------------------------------- /Library/Main/Processor/Processors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteHandler.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/9/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Processors { 12 | var createTableProcessor: CreateTableProcessor { 13 | CreateTableProcessor() 14 | } 15 | var saveProcessor: SaveProcessor { 16 | SaveProcessor() 17 | } 18 | var retrieveProcessor: RetrieveProcessor { 19 | RetrieveProcessor() 20 | } 21 | var updateProcessor: UpdateProcessor { 22 | UpdateProcessor() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Statements/SelectStatementTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectStatementTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | 6 | import XCTest 7 | @testable import SundeedQLiteLibrary 8 | 9 | final class SelectStatementTests: XCTestCase { 10 | func testSkipWithoutLimitAddsLimitMinusOne() { 11 | let query = StatementBuilder() 12 | .selectStatement(tableName: "T") 13 | .isOrdered(true) 14 | .orderBy(columnName: "integer") 15 | .isAscending(true) 16 | .skip(1) 17 | .build() 18 | XCTAssertNotNil(query) 19 | XCTAssertTrue(query?.contains(" LIMIT -1 OFFSET 1") == true, query ?? "") 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/xcshareddata/xcbaselines/06F8A1812349084B001D1381.xcbaseline/6FF2C487-D033-4241-8C96-0FC8B5152E67.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | OperationTests 8 | 9 | testStressTiming() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.52252 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/xcshareddata/xcbaselines/06F8A1812349084B001D1381.xcbaseline/1C3A1CCB-CE1E-4D96-BC40-278674C2DA72.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | SundeedQLiteLibraryTests 8 | 9 | testPerformanceExample() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 1 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Library/Main/Statements/StatementBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedStatements.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/9/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class StatementBuilder { 12 | func createTableStatement(tableName: String) -> CreateTableStatement { 13 | return CreateTableStatement(with: tableName) 14 | } 15 | func deleteStatement(tableName: String) -> DeleteStatement { 16 | return DeleteStatement(with: tableName) 17 | } 18 | func insertStatement(tableName: String) -> InsertStatement { 19 | return InsertStatement(with: tableName) 20 | } 21 | func updateStatement(tableName: String) -> UpdateStatement { 22 | return UpdateStatement(with: tableName) 23 | } 24 | func selectStatement(tableName: String) -> SelectStatement { 25 | return SelectStatement(with: tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | armv7 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /Library/ClassHandler/Map/SundeedQLiteMap+Ordering.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteMap+Ordering.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/16/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | postfix operator << 12 | public postfix func << (left: SundeedQLiteMap) -> SundeedQLiteMap { 13 | assert(!left.isOrdered, "You can only order by one column") 14 | guard let key = left.key else { 15 | fatalError("You can only order by one column") 16 | } 17 | left.orderBy = key 18 | left.asc = true 19 | left.isOrdered = true 20 | return left 21 | } 22 | 23 | postfix operator >> 24 | public postfix func >> (left: SundeedQLiteMap) -> SundeedQLiteMap { 25 | assert(!left.isOrdered, "You can only order by one column") 26 | guard let key = left.key else { 27 | fatalError("You can only order by one column") 28 | } 29 | left.orderBy = key 30 | left.asc = false 31 | left.isOrdered = true 32 | return left 33 | } 34 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Statements/StatementHelpersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatementHelpersTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | 6 | import XCTest 7 | @testable import SundeedQLiteLibrary 8 | 9 | final class StatementHelpersTests: XCTestCase { 10 | func testGetQuotationForTypes() { 11 | let s = Statement() 12 | XCTAssertEqual(s.getQuotation(forValue: "plain"), "\'") 13 | XCTAssertEqual(s.getQuotation(forValue: "a'b"), "\"") 14 | XCTAssertEqual(s.getQuotation(forValue: 1), "") 15 | XCTAssertEqual(s.getQuotation(forValue: 1.0 as Double), "") 16 | XCTAssertEqual(s.getQuotation(forValue: 1.0 as Float), "") 17 | XCTAssertEqual(s.getQuotation(forValue: true), "") 18 | XCTAssertEqual(s.getQuotation(forValue: Date()), "") 19 | } 20 | 21 | func testIsLastIndex() { 22 | let s = Statement() 23 | XCTAssertFalse(s.isLastIndex(index: 0, in: [1])) 24 | XCTAssertTrue(s.isLastIndex(index: 0, in: [1,2])) 25 | XCTAssertFalse(s.isLastIndex(index: 1, in: [1,2])) 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 SundeedQLite 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Library/Main/Processor/ObjectWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedObjectWrapper.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/11/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias SundeedObject = [String: Any] 12 | 13 | class ObjectWrapper { 14 | private(set) var tableName: String 15 | private(set) var className: String? 16 | var objects: SundeedObject? 17 | var types: [String: ParameterType]? 18 | var isOrdered: Bool 19 | var orderBy: String 20 | var asc: Bool 21 | var hasPrimaryKey: Bool 22 | init(tableName: String, 23 | className: String?, 24 | objects: SundeedObject?, 25 | types: [String: ParameterType]?, 26 | isOrdered: Bool = false, 27 | orderBy: String = "", 28 | asc: Bool = false, 29 | hasPrimaryKey: Bool = false) { 30 | self.tableName = tableName 31 | self.objects = objects 32 | self.types = types 33 | self.className = className 34 | self.isOrdered = isOrdered 35 | self.orderBy = orderBy 36 | self.asc = asc 37 | self.hasPrimaryKey = hasPrimaryKey 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | env: 8 | DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer 9 | jobs: 10 | build: 11 | runs-on: macOS-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Swift 15 | run: | 16 | sudo xcode-select -s /Applications/Xcode.app/Contents/Developer 17 | echo 'export PATH="/usr/bin:$PATH"' >> $GITHUB_PATH 18 | - name: Build and Test 19 | run: | 20 | xcodebuild clean build test -project "SundeedQLiteLibrary.xcodeproj" -scheme "SundeedQLiteLibrary" -destination "platform=iOS Simulator,OS=26.0.1,name=iPhone 17 Pro" -enableCodeCoverage YES GCC_GENERATE_TEST_COVERAGE_FILES=YES 21 | - name: Install Slather 22 | run: gem install slather 23 | - name: Convert .xcresult to .lcov 24 | run: slather coverage --cobertura-xml --input-format profdata --scheme SundeedQLiteLibrary --output-directory . --ignore "Pods/**/*" SundeedQLiteLibrary.xcodeproj 25 | - name: List files 26 | run: ls -ltr 27 | - name: Upload code coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | file: ./cobertura.xml 32 | fail_ci_if_error: true 33 | 34 | -------------------------------------------------------------------------------- /Library/Main/Processor/Processor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Processor.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 01/10/2025. 6 | // Copyright © 2025 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SQLite3 11 | 12 | class Processor { 13 | func getDatabaseColumns(forTable table: String) -> [(columnName: String, columnType: ParameterType)] { 14 | let database = SundeedQLiteConnection.pool.connection() 15 | var columnsStatement: OpaquePointer? 16 | var array: [(columnName: String, columnType: ParameterType)] = [] 17 | sqlite3_prepare_v2(database, 18 | "PRAGMA table_info(\(table));",-1, 19 | &columnsStatement, nil) 20 | while sqlite3_step(columnsStatement) == SQLITE_ROW { 21 | if let columnName = sqlite3_column_text(columnsStatement, 1), 22 | let columnType = sqlite3_column_text(columnsStatement, 2) { 23 | array.append((columnName: String(cString: columnName), 24 | columnType: ParameterType(typeString: String(cString: columnType)))) 25 | } 26 | } 27 | columnsStatement = nil 28 | SundeedQLiteConnection.pool.closeConnection(database) 29 | return array 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Library/Main/Statements/Statement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Statement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Statement { 12 | final func addSeparatorIfNeeded(separator: String, 13 | forStatement statement: inout String, 14 | needed: Bool) { 15 | statement.append(needed ? separator : "") 16 | } 17 | final func getQuotation(forValue value: Any) -> String { 18 | let characterSet = CharacterSet(charactersIn: "\'") 19 | if let value = value as? String { 20 | return value.rangeOfCharacter(from: characterSet) != nil ? "\"" : "\'" 21 | } else if let _ = value as? Date { 22 | return "" 23 | } else if let _ = value as? Int { 24 | return "" 25 | } else if let _ = value as? Double { 26 | return "" 27 | } else if let _ = value as? Float { 28 | return "" 29 | } else if let _ = value as? Bool { 30 | return "" 31 | } else { 32 | return "\"" 33 | } 34 | } 35 | final func isLastIndex(index: Int, in array: [T]) -> Bool { 36 | index != array.count - 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Library/Main/SundeedLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedLog.swift 3 | // SundeedQLite 4 | // 5 | // Created by Nour Sandid on 27/09/2025. 6 | // 7 | 8 | public enum SundeedLogLevel: Int { 9 | case verbose = 0 10 | case info = 1 11 | case error = 2 12 | case production = 3 13 | } 14 | 15 | class SundeedLogger { 16 | static var logLevel: SundeedLogLevel = .production 17 | static var testingDidPrint: Bool = false 18 | static func debug(_ items: Any..., separator: String = " ", terminator: String = "\n") { 19 | if logLevel.rawValue <= SundeedLogLevel.verbose.rawValue { 20 | testingDidPrint = true 21 | print(["SundeedQLiteDebug"] + items, separator: separator, terminator: terminator) 22 | } 23 | } 24 | static func info(_ items: Any..., separator: String = " ", terminator: String = "\n") { 25 | if logLevel.rawValue <= SundeedLogLevel.info.rawValue { 26 | testingDidPrint = true 27 | print(["SundeedQLiteInfo"] + items, separator: separator, terminator: terminator) 28 | } 29 | } 30 | static func error(_ items: Any..., separator: String = " ", terminator: String = "\n") { 31 | if logLevel.rawValue <= SundeedLogLevel.error.rawValue { 32 | testingDidPrint = true 33 | print(["SundeedQLiteError"] + items, separator: separator, terminator: terminator) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Library/Main/Statements/DeleteStatement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedDeleteStatement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DeleteStatement: Statement { 12 | let queue = DispatchQueue(label: "thread-safe-delete-statement", attributes: .concurrent) 13 | private var tableName: String 14 | private var filters: [SundeedExpression] = [] 15 | init(with tableName: String) { 16 | self.tableName = tableName 17 | } 18 | @discardableResult 19 | func withFilters(_ filters: [SundeedExpression?]) -> Self { 20 | queue.sync { 21 | self.filters = filters.compactMap({$0}) 22 | return self 23 | } 24 | } 25 | func build() -> String? { 26 | queue.sync { 27 | var statement: String = "DELETE FROM \(tableName) WHERE " 28 | addFilters(forStatement: &statement) 29 | SundeedLogger.debug("Delete Statement: \(statement)") 30 | return statement 31 | } 32 | } 33 | private func addFilters(forStatement statement: inout String) { 34 | queue.sync { 35 | if !filters.isEmpty { 36 | for (index, filter) in filters.enumerated() { 37 | statement.append(filter.toQuery()) 38 | addSeparatorIfNeeded(separator: " AND ", 39 | forStatement: &statement, 40 | needed: isLastIndex(index: index, in: filters)) 41 | } 42 | } else { 43 | statement.append("1") 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Library/Main/Statements/CreateTableStatement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedCreateTableStatement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/9/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CreateTableStatement { 12 | let queue = DispatchQueue(label: "thread-safe-create-table-statement", attributes: .concurrent) 13 | private var tableName: String 14 | private var hasPrimaryKey: Bool = false 15 | private var columns: [(name: String, type: String)] = [] 16 | init(with tableName: String) { 17 | self.tableName = tableName 18 | } 19 | @discardableResult 20 | func addColumn(with columnName: String, type: ParameterType) -> Self { 21 | queue.sync { 22 | columns.append((name: columnName, type: type.rawValue)) 23 | return self 24 | } 25 | } 26 | @discardableResult 27 | func withPrimaryKey() -> Self { 28 | self.hasPrimaryKey = true 29 | return self 30 | } 31 | func build() -> String? { 32 | queue.sync { 33 | var statement = "CREATE TABLE IF NOT EXISTS \(tableName) (\(Sundeed.shared.offlineID) INTEGER PRIMARY KEY, \(Sundeed.shared.foreignKey) TEXT, \(Sundeed.shared.fieldNameLink) TEXT" 34 | for column in columns { 35 | statement.append(", \(column.name) \(column.type)") 36 | } 37 | if hasPrimaryKey { 38 | statement.append(",CONSTRAINT unq\(tableName) UNIQUE (\(Sundeed.shared.foreignKey),\(Sundeed.shared.primaryKey),\(Sundeed.shared.fieldNameLink))") 39 | } 40 | statement.append(");") 41 | SundeedLogger.debug("Create Table Statement: \(statement)") 42 | return statement 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # Accio dependency management 52 | Dependencies/ 53 | .accio/ 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. 58 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots/**/*.png 65 | fastlane/test_output 66 | 67 | # Code Injection 68 | # 69 | # After new code Injection tools there's a generated folder /iOSInjectionProject 70 | # https://github.com/johnno1962/injectionforxcode 71 | 72 | iOSInjectionProject/ 73 | -------------------------------------------------------------------------------- /Library/Main/Sundeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sundeed.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Sundeed { 12 | static var shared: Sundeed = Sundeed() 13 | var tables: [String] = [] 14 | final let backgroundQueue: DispatchQueue = DispatchQueue( 15 | label: "globalBackgroundSyncronizeSharedData") 16 | /// VALUE 17 | final let valueColumnName: String = "VALUE" 18 | /// 19 | final let databaseNull: String = "" 20 | /// SUNDEED_UNIQUE_KEY 21 | final let primaryKey: String = "SUNDEED_UNIQUE_KEY" 22 | /// SUNDEED_FOREIGN_KEY 23 | final let foreignKey: String = "SUNDEED_FOREIGN_KEY" 24 | /// SUNDEED_FIELD_NAME_LINK 25 | final let fieldNameLink: String = "SUNDEED_FIELD_NAME_LINK" 26 | /// SUNDEED_OFFLINE_ID 27 | final let offlineID: String = "SUNDEED_OFFLINE_ID" 28 | /// SUNDEED_FOREIGN| 29 | final let foreignPrefix: String = "SUNDEED_FOREIGN|" 30 | /// SUNDEED_PRIMITIVE_FOREIGN| 31 | final let foreignPrimitivePrefix: String = "SUNDEED_PRIMITIVE_FOREIGN|" 32 | /// SQLiteDB.sqlite 33 | final let databaseFileName: String = "SQLiteDB.sqlite" 34 | /// SUNDEED_FOREIGN|#tableName#|#foreignKey# 35 | final func sundeedForeignValue(tableName: Any, 36 | fieldNameLink: String, 37 | subObjectPrimaryKey: String? = nil) -> String { 38 | if let subObjectPrimaryKey = subObjectPrimaryKey { 39 | return "\(foreignPrefix)\(tableName)|\(fieldNameLink)|\(subObjectPrimaryKey)" 40 | } else { 41 | return "\(foreignPrefix)\(tableName)|\(fieldNameLink)" 42 | } 43 | } 44 | /// SUNDEED_PRIMITIVE_FOREIGN|#tableName# 45 | final func sundeedPrimitiveForeignValue(tableName: Any) -> String { 46 | return "\(foreignPrimitivePrefix)\(tableName)" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Library/Main/SundeedQLiteError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedError.swift 3 | // SQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 12/15/18. 6 | // Copyright © 2018 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SundeedQLiteError: Error, CustomStringConvertible { 12 | case primaryKeyError(tableName: String) 13 | case unsupportedType(tableName: String, attribute: String) 14 | case noColumnWithThisName(tableName: String, columnName: String) 15 | case cantUseNameIndex(tableName: String) 16 | case noChangesMade(tableName: String) 17 | case noObjectPassed 18 | case cannotAccessDocumentDirectory 19 | case errorInConnection 20 | public var description: String { 21 | switch self { 22 | case .primaryKeyError(let tableName): 23 | return "Error with class \(tableName): \n No Primary Key \n - To add a primary key add a '+' sign in the mapping function in the class after the designated primary map \n e.g: self.id = map[\"ID\"]+" 24 | case .unsupportedType(let tableName, let attribute): 25 | return "Error with class \(tableName): \n Unsupported Type \(attribute) \n - Try to change the type of this attribute, or send us a suggestion so we can add it" 26 | case .noColumnWithThisName(let tableName, let columnName): 27 | return "Error with class \(tableName): \n No Column With Title \(columnName) \n - Try to change the column name and try again" 28 | case .noChangesMade(let tableName): 29 | return "Error with class \(tableName): \n Trying to perform global update statement with no changes \n - Try to add some changes and try again" 30 | case .cantUseNameIndex(let tableName): 31 | return "Error with class \(tableName): \n Unsupported column name \"index\" because it is reserved \n - Try to change it and try again" 32 | case .noObjectPassed: 33 | return "Error no object passed to handle" 34 | case .errorInConnection: 35 | return "Error with Connection: \n Unable to create a connection to the local database \n Make sure that you have access and permissions to device's files" 36 | case .cannotAccessDocumentDirectory: 37 | return "Error with Document Directory: Cannot Access Document Directory" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/xcshareddata/xcbaselines/06F8A1812349084B001D1381.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 1C3A1CCB-CE1E-4D96-BC40-278674C2DA72 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2300 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro14,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone11,8 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 6FF2C487-D033-4241-8C96-0FC8B5152E67 39 | 40 | localComputer 41 | 42 | busSpeedInMHz 43 | 100 44 | cpuCount 45 | 1 46 | cpuKind 47 | Dual-Core Intel Core i5 48 | cpuSpeedInMHz 49 | 2300 50 | logicalCPUCoresPerPackage 51 | 4 52 | modelCode 53 | MacBookPro14,1 54 | physicalCPUCoresPerPackage 55 | 2 56 | platformIdentifier 57 | com.apple.platform.macosx 58 | 59 | targetArchitecture 60 | x86_64 61 | targetDevice 62 | 63 | modelCode 64 | iPhone12,3 65 | platformIdentifier 66 | com.apple.platform.iphonesimulator 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Array Operator/ArrayMandatoryTestWithEmpty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayMandatoryTestWithEmpty.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SundeedQLiteLibrary 11 | 12 | class ArrayMandatoryTestWithEmpty: XCTestCase { 13 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 14 | Task { 15 | await ClassContainingAMandatoryArrayWithEmpty.delete() 16 | await ClassContainingAMandatoryOptionalArrayWithEmpty.delete() 17 | await ClassContainingAMandatoryOptionalArrayWithOptionalEmpty.delete() 18 | await ClassContainingAMandatoryArrayWithOptionalEmpty.delete() 19 | completion(nil) 20 | } 21 | } 22 | 23 | func testClassContainingAMandatoryArrayWithEmpty() async { 24 | let mainClass = ClassContainingAMandatoryArrayWithEmpty() 25 | mainClass.mandatoryClasses = [MandatoryClass()] 26 | 27 | await mainClass.save() 28 | let retrievedClasses = await ClassContainingAMandatoryArrayWithEmpty.retrieve() 29 | XCTAssertEqual(retrievedClasses.count, 0) 30 | } 31 | 32 | func testClassContainingAMandatoryOptionalArrayWithEmpty() async { 33 | let mainClass = ClassContainingAMandatoryOptionalArrayWithEmpty() 34 | 35 | await mainClass.save() 36 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithEmpty.retrieve() 37 | XCTAssertEqual(retrievedClasses.count, 0) 38 | } 39 | 40 | func testClassContainingAMandatoryOptionalArrayWithOptionalEmpty() async { 41 | let mainClass = ClassContainingAMandatoryOptionalArrayWithOptionalEmpty() 42 | mainClass.mandatoryClasses = [] 43 | 44 | await mainClass.save() 45 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithOptionalEmpty.retrieve() 46 | XCTAssertEqual(retrievedClasses.count, 0) 47 | } 48 | 49 | func testClassContainingAMandatoryArrayWithOptionalEmpty() async { 50 | let mainClass = ClassContainingAMandatoryArrayWithOptionalEmpty() 51 | mainClass.mandatoryClasses = [MandatoryClass()] 52 | 53 | await mainClass.save() 54 | let retrievedClasses = await ClassContainingAMandatoryArrayWithOptionalEmpty.retrieve() 55 | XCTAssertEqual(retrievedClasses.count, 0) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Library/Main/Statements/InsertStatement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedInsertStatement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class InsertStatement: Statement { 12 | let queue = DispatchQueue(label: "thread-safe-insert-statement", attributes: .concurrent) 13 | private var tableName: String 14 | private var columns: [String: ParameterType] 15 | private var keyValues: [(String, Any?)] = [] 16 | private var values: [ParameterType] = [] 17 | init(with tableName: String) { 18 | self.tableName = tableName 19 | self.columns = Processor().getDatabaseColumns(forTable: tableName).reduce(into: [String: ParameterType]()) { result, element in 20 | result[element.columnName] = element.columnType 21 | } 22 | } 23 | @discardableResult 24 | func add(key: String, value: Any?) -> Self { 25 | queue.sync { 26 | keyValues.append((key, value)) 27 | return self 28 | } 29 | } 30 | func build() -> (query: String, parameters: [ParameterType])? { 31 | queue.sync { 32 | guard !keyValues.isEmpty else { return nil } 33 | var statement: String = "REPLACE INTO \(tableName) (" 34 | addKeysAndValues(toStatement: &statement) 35 | SundeedLogger.debug("Insert Statement: \(statement), with parameters: \(values)") 36 | return (query: statement, parameters: values) 37 | } 38 | } 39 | private func addKeysAndValues(toStatement statement: inout String) { 40 | queue.sync { 41 | var valuesStatement: String = ") VALUES (" 42 | for (index, (key, value)) in keyValues.enumerated() { 43 | statement.append(key) 44 | valuesStatement.append("?") 45 | let columnType = self.columns[key] ?? .text("") 46 | values.append(columnType.withValue(value)) 47 | let needed = isLastIndex(index: index, in: keyValues) 48 | addSeparatorIfNeeded(separator: ", ", 49 | forStatement: &statement, 50 | needed: needed) 51 | addSeparatorIfNeeded(separator: ", ", 52 | forStatement: &valuesStatement, 53 | needed: needed) 54 | } 55 | valuesStatement.append(");") 56 | statement = "\(statement)\(valuesStatement)" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Operations/RetrieveWithLimitAndSkip.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetrieveWithLimitAndSkip.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Tests on 10/31/2025. 6 | // 7 | 8 | import XCTest 9 | import SundeedQLiteLibrary 10 | 11 | class RetrieveWithLimitAndSkip: XCTestCase { 12 | var employer1: EmployerForTesting = EmployerForTesting() 13 | var employer2: EmployerForTesting = EmployerForTesting() 14 | var employer3: EmployerForTesting = EmployerForTesting() 15 | 16 | override func setUp() { 17 | employer1.fillData() 18 | employer1.integer = 1 19 | employer1.string = "emp1" 20 | 21 | employer2.fillData() 22 | employer2.integer = 2 23 | employer2.string = "emp2" 24 | 25 | employer3.fillData() 26 | employer3.integer = 3 27 | employer3.string = "emp3" 28 | } 29 | 30 | override func tearDown() async throws { 31 | await EmployerForTesting.delete() 32 | await EmployeeForTesting.delete() 33 | } 34 | 35 | func testRetrieveWithLimitOnly() async { 36 | await [employer1, employer2, employer3].save() 37 | let results = await EmployerForTesting.retrieve(withFilter: nil, 38 | orderBy: SundeedColumn("integer"), 39 | ascending: true, 40 | limit: 1) 41 | XCTAssertEqual(results.count, 1) 42 | XCTAssertEqual(results.first?.string, "emp1") 43 | } 44 | 45 | func testRetrieveWithSkipOnly() async { 46 | await [employer1, employer2, employer3].save() 47 | let results = await EmployerForTesting.retrieve(orderBy: SundeedColumn("integer"), 48 | ascending: true, 49 | skip: 1) 50 | XCTAssertEqual(results.count, 2) 51 | XCTAssertEqual(results.first?.string, "emp2") 52 | } 53 | 54 | func testRetrieveWithSkipAndLimitOrdered() async { 55 | await [employer1, employer2, employer3].save() 56 | let results = await EmployerForTesting.retrieve(withFilter: nil, 57 | orderBy: SundeedColumn("integer"), 58 | ascending: true, 59 | limit: 2, 60 | skip: 1) 61 | XCTAssertEqual(results.count, 2) 62 | XCTAssertEqual(results[0].string, "emp2") 63 | XCTAssertEqual(results[1].string, "emp3") 64 | } 65 | } 66 | 67 | 68 | -------------------------------------------------------------------------------- /Library/ClassHandler/Map/SundeedQLiteMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteMap.swift 3 | // SQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 12/9/18. 6 | // Copyright © 2018 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class SundeedQLiteMap { 12 | var map: [String: Any] = [:] 13 | var columns: [String: AnyObject] = [:] 14 | var types: [String: ParameterType] = [:] 15 | var fetchingColumns: Bool = false 16 | var key: String? 17 | var currentValue: Any? 18 | static var references: [String: [String: SundeedQLiter]] = [:] 19 | var primaryKey: String = "" 20 | var orderBy: String = "" 21 | var asc: Bool = true 22 | var isOrdered: Bool = false 23 | var hasPrimaryKey: Bool = false 24 | var isSafeToAdd: Bool = true 25 | public subscript(key: String) -> SundeedQLiteMap { 26 | self.key = key 27 | if map.contains(where: { (key1, _) -> Bool in 28 | return key1 == key 29 | }) { 30 | self.currentValue = map[key] 31 | } else { 32 | self.currentValue = nil 33 | } 34 | return self 35 | } 36 | func addColumn(attribute: T, withColumnName columnName: String, type: ParameterType) { 37 | self.columns[columnName] = attribute as AnyObject 38 | self.types[columnName] = type 39 | if hasPrimaryKey && columnName == primaryKey { 40 | self.columns[Sundeed.shared.primaryKey] = attribute as AnyObject 41 | } 42 | } 43 | init(fetchingColumns: Bool) { 44 | self.fetchingColumns = fetchingColumns 45 | } 46 | init(dictionnary: [String: Any]) { 47 | self.map = dictionnary 48 | self.fetchingColumns = false 49 | } 50 | static func addReference(object: SundeedQLiter, 51 | andValue value: AnyObject, 52 | andClassName className: String) { 53 | if SundeedQLiteMap.references[className] == nil { 54 | SundeedQLiteMap.references[className] = [:] 55 | } 56 | if SundeedQLiteMap.references[className]?["\(value)"] == nil { 57 | SundeedQLiteMap.references[className]?["\(value)"] = object 58 | } 59 | } 60 | static func getReference(andValue value: AnyObject, 61 | andClassName name: String) -> SundeedQLiter? { 62 | if SundeedQLiteMap.references[name] == nil { 63 | SundeedQLiteMap.references[name] = [:] 64 | } 65 | return SundeedQLiteMap.references[name]?["\(value)"] 66 | } 67 | static func removeReference(value: AnyObject, 68 | andClassName className: String) { 69 | SundeedQLiteMap.references[className]?["\(value)"] = nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/SundeedLog/SundeedLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedLog.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 02/10/2025. 6 | // Copyright © 2025 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class SundeedLoggerTests: XCTestCase { 13 | override func setUp() { 14 | SundeedLogger.testingDidPrint = false 15 | } 16 | 17 | func testDebugLogWithVerboseLevel() { 18 | SundeedLogger.logLevel = .verbose 19 | SundeedLogger.debug("") 20 | XCTAssertTrue(SundeedLogger.testingDidPrint) 21 | } 22 | 23 | func testInfoLogWithVerboseLevel() { 24 | SundeedLogger.logLevel = .verbose 25 | SundeedLogger.info("") 26 | XCTAssertTrue(SundeedLogger.testingDidPrint) 27 | } 28 | 29 | func testErrorLogWithVerboseLevel() { 30 | SundeedLogger.logLevel = .verbose 31 | SundeedLogger.info("") 32 | XCTAssertTrue(SundeedLogger.testingDidPrint) 33 | } 34 | 35 | func testDebugLogWithInfoLevel() { 36 | SundeedLogger.logLevel = .info 37 | SundeedLogger.debug("") 38 | XCTAssertFalse(SundeedLogger.testingDidPrint) 39 | } 40 | 41 | func testInfoLogWithInfoLevel() { 42 | SundeedLogger.logLevel = .info 43 | SundeedLogger.info("") 44 | XCTAssertTrue(SundeedLogger.testingDidPrint) 45 | } 46 | 47 | func testErrorLogWithInfoLevel() { 48 | SundeedLogger.logLevel = .info 49 | SundeedLogger.info("") 50 | XCTAssertTrue(SundeedLogger.testingDidPrint) 51 | } 52 | 53 | func testDebugLogWithErrorLevel() { 54 | SundeedLogger.logLevel = .error 55 | SundeedLogger.debug("") 56 | XCTAssertFalse(SundeedLogger.testingDidPrint) 57 | } 58 | 59 | func testInfoLogWithErrorLevel() { 60 | SundeedLogger.logLevel = .error 61 | SundeedLogger.info("") 62 | XCTAssertFalse(SundeedLogger.testingDidPrint) 63 | } 64 | 65 | func testErrorLogWithErrorLevel() { 66 | SundeedLogger.logLevel = .error 67 | SundeedLogger.error("") 68 | XCTAssertTrue(SundeedLogger.testingDidPrint) 69 | } 70 | 71 | func testDebugLogWithProductionLevel() { 72 | SundeedLogger.logLevel = .production 73 | SundeedLogger.debug("") 74 | XCTAssertFalse(SundeedLogger.testingDidPrint) 75 | } 76 | 77 | func testInfoLogWithProductionLevel() { 78 | SundeedLogger.logLevel = .production 79 | SundeedLogger.info("") 80 | XCTAssertFalse(SundeedLogger.testingDidPrint) 81 | } 82 | 83 | func testErrorLogWithProductionLevel() { 84 | SundeedLogger.logLevel = .production 85 | SundeedLogger.info("") 86 | XCTAssertFalse(SundeedLogger.testingDidPrint) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/No Primary Keys/OperationTestsWithoutPrimaryKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationTestsWithoutPrimaryKey.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/16/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SundeedQLiteLibrary 11 | 12 | class OperationTestsWithoutPrimaryKey: XCTestCase { 13 | var noPrimary: ClassWithNoPrimary? 14 | var employerWithNoPrimary: EmployerWithNoPrimaryForTesting? 15 | 16 | 17 | override func setUp() { 18 | employerWithNoPrimary = EmployerWithNoPrimaryForTesting() 19 | employerWithNoPrimary?.fillData() 20 | noPrimary = ClassWithNoPrimary() 21 | noPrimary?.fillData() 22 | } 23 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 24 | Task { 25 | await EmployerWithNoPrimaryForTesting.delete() 26 | await ClassWithNoPrimary.delete() 27 | noPrimary = nil 28 | employerWithNoPrimary = nil 29 | completion(nil) 30 | } 31 | } 32 | 33 | func testSaveEmployerWithNoPrimary() async { 34 | await employerWithNoPrimary?.save() 35 | let employers = await EmployerWithNoPrimaryForTesting.retrieve() 36 | XCTAssert(employers.isEmpty) 37 | } 38 | 39 | 40 | func testRetrieve() async { 41 | 42 | await noPrimary?.save() 43 | let noPrimaries = await ClassWithNoPrimary.retrieve() 44 | guard let noPrimary = noPrimaries.first else { 45 | XCTFail("Couldn't Retrieve From Database") 46 | return 47 | } 48 | XCTAssertEqual(noPrimary.firstName, "TestFirst") 49 | XCTAssertEqual(noPrimary.lastName, "TestLast") 50 | } 51 | 52 | func testRetrieveWithFilter() async { 53 | 54 | await noPrimary?.save() 55 | let noPrimaries = await ClassWithNoPrimary.retrieve(withFilter: SundeedColumn("firstName") == "TestFirst") 56 | 57 | guard let noPrimary = noPrimaries.first else { 58 | XCTFail("Couldn't Retrieve From Database") 59 | return 60 | } 61 | XCTAssertEqual(noPrimary.firstName, "TestFirst") 62 | XCTAssertEqual(noPrimary.lastName, "TestLast") 63 | } 64 | 65 | func testUpdate() async { 66 | await noPrimary?.save() 67 | do { 68 | self.noPrimary?.firstName = "TestFirstUpdated" 69 | try await self.noPrimary?.update(columns: SundeedColumn("firstName")) 70 | XCTFail("It shouldn't be able to update") 71 | } catch { 72 | XCTAssertTrue(true) 73 | } 74 | } 75 | 76 | func testGlobalUpdate() async { 77 | 78 | await noPrimary?.save() 79 | do { 80 | try await ClassWithNoPrimary.update(changes: SundeedColumn("firstName") <~ "TestFirstUpdated") 81 | XCTFail("It shouldn't be able to update") 82 | } catch { 83 | XCTAssertTrue(true) 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/No Primary Keys/NoPrimariesClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoPrimariesClasses.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class ClassWithNoPrimaryWithImage: @unchecked Sendable, SundeedQLiter { 13 | var image: UIImage? 14 | required init() {} 15 | 16 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 17 | image <~> map["image"] 18 | } 19 | 20 | func fillData() { 21 | image = UIImage(named: "image") 22 | } 23 | } 24 | 25 | class ClassWithNoPrimaryWithImageArray: @unchecked Sendable, SundeedQLiter { 26 | var images: [UIImage?]? 27 | required init() {} 28 | 29 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 30 | images <~> map["images"] 31 | } 32 | 33 | func fillData() { 34 | images = [UIImage(named: "image")] 35 | } 36 | } 37 | 38 | class ClassWithNoPrimaryWithPrimitiveArray: @unchecked Sendable, SundeedQLiter { 39 | var strings: [String]? 40 | required init() {} 41 | 42 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 43 | strings <~> map["strings"] 44 | } 45 | 46 | func fillData() { 47 | strings = ["test"] 48 | } 49 | } 50 | 51 | class ClassWithNoPrimaryWithDate: @unchecked Sendable, SundeedQLiter { 52 | var date: Date? 53 | required init() {} 54 | 55 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 56 | date <~> map["date"] 57 | } 58 | 59 | func fillData() { 60 | date = Date() 61 | } 62 | } 63 | 64 | 65 | class ClassWithNoPrimaryWithSubClass: @unchecked Sendable, SundeedQLiter { 66 | var object: EmployeeForTesting? 67 | required init() {} 68 | 69 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 70 | object <~> map["object"] 71 | } 72 | 73 | func fillData() { 74 | object = EmployeeForTesting(id: "TestID", seniorID: "TestID1", juniorID: "TestID2") 75 | } 76 | 77 | } 78 | 79 | class ClassWithNoPrimaryWithSubClassArray: @unchecked Sendable, SundeedQLiter { 80 | var objects: [EmployeeForTesting?]? 81 | required init() {} 82 | 83 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 84 | objects <~> map["objects"] 85 | } 86 | 87 | func fillData() { 88 | objects = [EmployeeForTesting(id: "TestID", seniorID: "TestID1", juniorID: "TestID2")] 89 | } 90 | 91 | } 92 | 93 | 94 | class ClassWithNoPrimary: @unchecked Sendable, SundeedQLiter { 95 | var firstName: String? 96 | var lastName: String? 97 | required init() {} 98 | 99 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 100 | firstName <~> map["firstName"] 101 | lastName <~> map["lastName"] 102 | } 103 | 104 | func fillData() { 105 | firstName = "TestFirst" 106 | lastName = "TestLast" 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Library/Main/Statements/UpdateStatement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedUpdateStatement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UpdateStatement: Statement { 12 | let queue = DispatchQueue(label: "thread-safe-update-statement", attributes: .concurrent) 13 | private var tableName: String 14 | private var columns: [String: ParameterType] 15 | private var keyValues: [(String, Any?)] = [] 16 | private var values: [ParameterType] = [] 17 | private var filters: [SundeedExpression] = [] 18 | init(with tableName: String) { 19 | self.tableName = tableName 20 | self.columns = Processor().getDatabaseColumns(forTable: tableName).reduce(into: [String: ParameterType]()) { result, element in 21 | result[element.columnName] = element.columnType 22 | } 23 | } 24 | @discardableResult 25 | func add(key: String, value: Any?) -> Self { 26 | queue.sync { 27 | keyValues.append((key, value)) 28 | return self 29 | } 30 | } 31 | @discardableResult 32 | func withFilters(_ filters: [SundeedExpression?]) -> Self { 33 | queue.sync { 34 | self.filters = filters.compactMap({$0}) 35 | return self 36 | } 37 | } 38 | func build() -> (query: String, parameters: [ParameterType])? { 39 | queue.sync { 40 | guard !keyValues.isEmpty else { return nil } 41 | var statement = "UPDATE \(tableName) SET " 42 | addKeyValues(toStatement: &statement) 43 | addFilters(toStatement: &statement) 44 | SundeedLogger.debug("Update Statement: \(statement), with parameters \(values)") 45 | return (query: statement, parameters: values) 46 | } 47 | } 48 | private func addKeyValues(toStatement statement: inout String) { 49 | queue.sync { 50 | for (index, (key, value)) in keyValues.enumerated() where self.columns[key] != nil { 51 | statement.append("\(key) = ?") 52 | let columnType = self.columns[key] ?? .text("") 53 | values.append(columnType.withValue(value)) 54 | addSeparatorIfNeeded(separator: ", ", 55 | forStatement: &statement, 56 | needed: isLastIndex(index: index, in: keyValues)) 57 | } 58 | } 59 | } 60 | private func addFilters(toStatement statement: inout String) { 61 | queue.sync { 62 | statement.append(" WHERE ") 63 | if !filters.isEmpty { 64 | for (index, filter) in filters.enumerated() { 65 | statement.append(filter.toQuery()) 66 | addSeparatorIfNeeded(separator: " AND ", 67 | forStatement: &statement, 68 | needed: isLastIndex(index: index, in: filters)) 69 | } 70 | } else { 71 | statement.append("1") 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Library/Main/SundeedQLiterLibrary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiterLibrary.swift 3 | // SQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 12/9/18. 6 | // Copyright © 2018 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SQLite3 11 | 12 | infix operator <~ 13 | /** Setting variables in global update statement in local database */ 14 | public func <~ (left: SundeedColumn, right: String) -> SundeedUpdateSetStatement { 15 | return SundeedUpdateSetStatement(sundeedColumn: left, withValue: right as AnyObject) 16 | } 17 | final public class SundeedColumn: Sendable { 18 | let value: String 19 | required public init(_ value: String) { 20 | self.value = value 21 | } 22 | public typealias StringLiteralType = String 23 | } 24 | /** SundeedColumn("columnName") <~ "value" */ 25 | public struct SundeedUpdateSetStatement { 26 | public var column: SundeedColumn 27 | public var value: AnyObject 28 | public init(sundeedColumn column: SundeedColumn, withValue value: AnyObject) { 29 | self.column = column 30 | self.value = value 31 | } 32 | } 33 | 34 | extension UIImage { 35 | static func fromDatatypeValue(filePath: String) -> UIImage? { 36 | if let documentsDirectoryURL = FileManager 37 | .default.urls(for: .documentDirectory, 38 | in: .userDomainMask) 39 | .first { 40 | let fileURL = documentsDirectoryURL.appendingPathComponent("SundeedQLite/Image", isDirectory: true).appendingPathComponent(filePath) 41 | do { 42 | if FileManager.default.fileExists(atPath: fileURL.path) { 43 | return try autoreleasepool { 44 | let data = try Data(contentsOf: fileURL) 45 | SundeedLogger.debug("SundeedQLite: Fetching image from \(fileURL.absoluteString)") 46 | return UIImage(data: data) 47 | } 48 | } 49 | } catch { 50 | SundeedLogger.error(error) 51 | } 52 | } 53 | return nil 54 | } 55 | func dataTypeValue(forObjectID objectID: String) -> String { 56 | guard let documentsDirectoryURL = FileManager 57 | .default.urls(for: .documentDirectory, in: .userDomainMask) 58 | .first else { 59 | fatalError("Unable to access document directory") 60 | } 61 | 62 | let directoryURL = documentsDirectoryURL.appendingPathComponent("SundeedQLite/Image", isDirectory: true) 63 | let fileURL = directoryURL.appendingPathComponent("\(objectID).png") 64 | 65 | do { 66 | try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) 67 | SundeedLogger.debug("Saving image to \(fileURL.absoluteString)") 68 | try self.pngData()?.write(to: fileURL, options: .completeFileProtectionUnlessOpen) 69 | SundeedLogger.debug("Image Saved to \(fileURL.absoluteString)") 70 | } catch { 71 | SundeedLogger.error(error) 72 | SundeedLogger.debug("Error saving PNG: \(error) \(#file) \(#line)") 73 | } 74 | 75 | return fileURL.lastPathComponent 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/ErrorCodes/SundeedQLiteErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteErrorTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 10/8/19. 6 | // Copyright © 2019 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class SundeedQLiteErrorTests: XCTestCase { 13 | var error: SundeedQLiteError? 14 | var employer: EmployerForTesting = EmployerForTesting() 15 | 16 | override func setUp() { 17 | self.employer.fillData() 18 | } 19 | override func tearDown() async throws { 20 | try await self.employer.delete(deleteSubObjects: true) 21 | error = nil 22 | } 23 | 24 | func testPrimaryKeyError() { 25 | error = .primaryKeyError(tableName: employer.getTableName()) 26 | let errorString = "Error with class \(employer.getTableName()): \n No Primary Key \n - To add a primary key add a '+' sign in the mapping function in the class after the designated primary map \n e.g: self.id = map[\"ID\"]+" 27 | XCTAssertEqual(error?.description, errorString) 28 | } 29 | 30 | func testUnsupportedTypeError() { 31 | error = .unsupportedType(tableName: employer.getTableName(), attribute: "Test") 32 | let errorString = "Error with class \(employer.getTableName()): \n Unsupported Type Test \n - Try to change the type of this attribute, or send us a suggestion so we can add it" 33 | XCTAssertEqual(error?.description, errorString) 34 | } 35 | 36 | func testNoColumnWithThisNameError() { 37 | error = .noColumnWithThisName(tableName: employer.getTableName(), columnName: "Test") 38 | let errorString = "Error with class \(employer.getTableName()): \n No Column With Title Test \n - Try to change the column name and try again" 39 | XCTAssertEqual(error?.description, errorString) 40 | } 41 | 42 | func testCantUseNameIndexError() { 43 | error = .cantUseNameIndex(tableName: employer.getTableName()) 44 | let errorString = "Error with class \(employer.getTableName()): \n Unsupported column name \"index\" because it is reserved \n - Try to change it and try again" 45 | XCTAssertEqual(error?.description, errorString) 46 | } 47 | 48 | func testNoChangesMadeError() { 49 | error = .noChangesMade(tableName: employer.getTableName()) 50 | let errorString = "Error with class \(employer.getTableName()): \n Trying to perform global update statement with no changes \n - Try to add some changes and try again" 51 | XCTAssertEqual(error?.description, errorString) 52 | } 53 | 54 | func testErrorInNoObjectError() { 55 | error = .noObjectPassed 56 | let errorString = "Error no object passed to handle" 57 | XCTAssertEqual(error?.description, errorString) 58 | } 59 | 60 | func testErrorInConnectionError() { 61 | error = .errorInConnection 62 | let errorString = "Error with Connection: \n Unable to create a connection to the local database \n Make sure that you have access and permissions to device's files" 63 | XCTAssertEqual(error?.description, errorString) 64 | } 65 | 66 | func testErrorInAccessDocumentDirectory() { 67 | error = .cannotAccessDocumentDirectory 68 | let errorString = "Error with Document Directory: Cannot Access Document Directory" 69 | XCTAssertEqual(error?.description, errorString) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SundeedQLiteLibrary.xcodeproj/xcshareddata/xcschemes/SundeedQLiteLibrary.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Array Operator/ArrayMandatoryTestWithNil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayMandatoryTestWithNil.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SundeedQLiteLibrary 11 | 12 | class ArrayMandatoryTestWithNil: XCTestCase { 13 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 14 | Task { 15 | await ClassContainingAMandatoryOptionalArrayWithNil.delete() 16 | await ClassContainingAMandatoryArrayWithNil.delete() 17 | await ClassContainingAMandatoryArrayWithOptionalNil.delete() 18 | await ClassContainingAMandatoryOptionalArrayWithOptionalNil.delete() 19 | completion(nil) 20 | } 21 | } 22 | 23 | func testClassContainingAMandatoryOptionalArrayWithNil() async { 24 | let mainClass = ClassContainingAMandatoryOptionalArrayWithNil() 25 | 26 | await mainClass.save() 27 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithNil.retrieve() 28 | XCTAssertEqual(retrievedClasses.count, 0) 29 | } 30 | 31 | func testClassContainingAMandatoryArrayWithNil() async { 32 | let mainClass = ClassContainingAMandatoryArrayWithNil() 33 | 34 | await mainClass.save() 35 | let retrievedClasses = await ClassContainingAMandatoryArrayWithNil.retrieve() 36 | XCTAssertEqual(retrievedClasses.count, 0) 37 | } 38 | 39 | func testClassContainingAMandatoryArrayWithOptionalNil() async { 40 | let mainClass = ClassContainingAMandatoryArrayWithOptionalNil() 41 | 42 | await mainClass.save() 43 | let retrievedClasses = await ClassContainingAMandatoryArrayWithOptionalNil.retrieve() 44 | XCTAssertEqual(retrievedClasses.count, 0) 45 | } 46 | 47 | func testClassContainingAMandatoryOptionalArrayWithOptionalNil() async { 48 | let mainClass = ClassContainingAMandatoryOptionalArrayWithOptionalNil() 49 | 50 | await mainClass.save() 51 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithOptionalNil.retrieve() 52 | XCTAssertEqual(retrievedClasses.count, 0) 53 | } 54 | 55 | func testClassContainingAMandatoryOptionalArrayWithNilSubObject() async { 56 | let mainClass = ClassContainingAMandatoryOptionalArrayWithNil() 57 | let mandatoryClass = MandatoryClass() 58 | mainClass.mandatoryClasses = [mandatoryClass] 59 | 60 | await mainClass.save() 61 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithNil.retrieve() 62 | XCTAssertEqual(retrievedClasses.count, 0) 63 | } 64 | 65 | func testClassContainingAMandatoryArrayWithNilSubObject() async { 66 | let mainClass = ClassContainingAMandatoryArrayWithNil() 67 | let mandatoryClass = MandatoryClass() 68 | mainClass.mandatoryClasses = [mandatoryClass] 69 | 70 | await mainClass.save() 71 | let retrievedClasses = await ClassContainingAMandatoryArrayWithNil.retrieve() 72 | XCTAssertEqual(retrievedClasses.count, 0) 73 | } 74 | 75 | func testClassContainingAMandatoryArrayWithOptionalNilSubObject() async { 76 | let mainClass = ClassContainingAMandatoryArrayWithOptionalNil() 77 | let mandatoryClass = MandatoryClass() 78 | mainClass.mandatoryClasses = [mandatoryClass] 79 | 80 | await mainClass.save() 81 | let retrievedClasses = await ClassContainingAMandatoryArrayWithOptionalNil.retrieve() 82 | XCTAssertEqual(retrievedClasses.count, 0) 83 | } 84 | 85 | func testClassContainingAMandatoryOptionalArrayWithOptionalNilSubObject() async { 86 | let mainClass = ClassContainingAMandatoryOptionalArrayWithOptionalNil() 87 | let mandatoryClass = MandatoryClass() 88 | mainClass.mandatoryClasses = [mandatoryClass] 89 | 90 | await mainClass.save() 91 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithOptionalNil.retrieve() 92 | XCTAssertEqual(retrievedClasses.count, 0) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Array Operator/ArrayMandatoryClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayMandatoryClasses.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import SundeedQLiteLibrary 11 | 12 | class ClassContainingAMandatoryOptionalArrayWithNil: @unchecked Sendable, SundeedQLiter { 13 | var id: String = "ID" 14 | var mandatoryClasses: [MandatoryClass]? 15 | required init() {} 16 | 17 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 18 | id <~> map["id"]+ 19 | mandatoryClasses <**> map["mandatoryClasses"] 20 | } 21 | } 22 | 23 | class ClassContainingAMandatoryArrayWithNil: @unchecked Sendable, SundeedQLiter { 24 | var id: String = "ID" 25 | var mandatoryClasses: [MandatoryClass] = [] 26 | required init() {} 27 | 28 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 29 | id <~> map["id"]+ 30 | mandatoryClasses <**> map["mandatoryClasses"] 31 | } 32 | } 33 | 34 | class ClassContainingAMandatoryOptionalArrayWithOptionalNil: @unchecked Sendable, SundeedQLiter { 35 | var id: String = "ID" 36 | var mandatoryClasses: [MandatoryClass?]? 37 | required init() {} 38 | 39 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 40 | id <~> map["id"]+ 41 | mandatoryClasses <**> map["mandatoryClasses"] 42 | } 43 | } 44 | 45 | class ClassContainingAMandatoryArrayWithOptionalNil: @unchecked Sendable, SundeedQLiter { 46 | var id: String = "ID" 47 | var mandatoryClasses: [MandatoryClass?] = [] 48 | required init() {} 49 | 50 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 51 | id <~> map["id"]+ 52 | mandatoryClasses <**> map["mandatoryClasses"] 53 | } 54 | } 55 | 56 | class ClassContainingAMandatoryArrayWithEmpty: @unchecked Sendable, SundeedQLiter { 57 | var id: String = "ID" 58 | var mandatoryClasses: [MandatoryClass] = [] 59 | required init() {} 60 | 61 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 62 | id <~> map["id"]+ 63 | mandatoryClasses <**> map["mandatoryClasses"] 64 | } 65 | } 66 | 67 | class ClassContainingAMandatoryOptionalArrayWithOptionalEmpty: @unchecked Sendable, SundeedQLiter { 68 | var id: String = "ID" 69 | var mandatoryClasses: [MandatoryClass?]? 70 | required init() {} 71 | 72 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 73 | id <~> map["id"]+ 74 | mandatoryClasses <**> map["mandatoryClasses"] 75 | } 76 | } 77 | 78 | class ClassContainingAMandatoryArrayWithOptionalEmpty: @unchecked Sendable, SundeedQLiter { 79 | var id: String = "ID" 80 | var mandatoryClasses: [MandatoryClass?] = [] 81 | required init() {} 82 | 83 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 84 | id <~> map["id"]+ 85 | mandatoryClasses <**> map["mandatoryClasses"] 86 | } 87 | } 88 | 89 | class ClassContainingAMandatoryOptionalArrayWithEmpty: @unchecked Sendable, SundeedQLiter { 90 | var id: String = "ID" 91 | var mandatoryClasses: [MandatoryClass]? 92 | required init() {} 93 | 94 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 95 | id <~> map["id"]+ 96 | mandatoryClasses <**> map["mandatoryClasses"] 97 | } 98 | } 99 | 100 | class ClassContainingAMandatoryArrayWithData: @unchecked Sendable, SundeedQLiter { 101 | var id: String = "ID" 102 | var mandatoryClasses: [MandatoryClass] = [] 103 | required init() {} 104 | 105 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 106 | id <~> map["id"]+ 107 | mandatoryClasses <**> map["mandatoryClasses"] 108 | } 109 | } 110 | 111 | class ClassContainingAMandatoryOptionalArrayWithData: @unchecked Sendable, SundeedQLiter { 112 | var id: String = "ID" 113 | var mandatoryClasses: [MandatoryClass]? 114 | required init() {} 115 | 116 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 117 | id <~> map["id"]+ 118 | mandatoryClasses <**> map["mandatoryClasses"] 119 | } 120 | } 121 | 122 | class ClassContainingAMandatoryOptionalArrayWithOptionalData: @unchecked Sendable, SundeedQLiter { 123 | var id: String = "ID" 124 | var mandatoryClasses: [MandatoryClass?]? 125 | required init() {} 126 | 127 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 128 | id <~> map["id"]+ 129 | mandatoryClasses <**> map["mandatoryClasses"] 130 | } 131 | } 132 | 133 | class ClassContainingAMandatoryArrayWithOptionalData: @unchecked Sendable, SundeedQLiter { 134 | var id: String = "ID" 135 | var mandatoryClasses: [MandatoryClass?] = [] 136 | required init() {} 137 | 138 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 139 | id <~> map["id"]+ 140 | mandatoryClasses <**> map["mandatoryClasses"] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Library/Main/Processor/Processors/CreateTableProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedCreateTableProcessor.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/11/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CreateTableProcessor: Processor { 12 | func createTableIfNeeded(for object: ObjectWrapper?) throws { 13 | guard let object = object, 14 | let objects = object.objects else { 15 | throw SundeedQLiteError.noObjectPassed 16 | } 17 | if !Sundeed.shared.tables.contains(object.tableName) { 18 | SundeedLogger.info("Creating table for \(object.tableName)") 19 | let createTableStatement = StatementBuilder() 20 | .createTableStatement(tableName: object.tableName) 21 | for (columnName, attribute) in objects { 22 | if let attribute = attribute as? ObjectWrapper { 23 | try createTableIfNeeded(for: attribute) 24 | } else if let attribute = attribute as? [ObjectWrapper] { 25 | if let firstAttribute = attribute.first { 26 | try createTableIfNeeded(for: firstAttribute) 27 | } 28 | } else if attribute is [Any] { 29 | if let array = attribute as? [Any], let _ = array.first as? Data { 30 | createTableForPrimitiveDataTypes(withTableName: columnName, type: .blob(nil)) 31 | } else if let array = attribute as? [Any], let _ = array.first as? Date { 32 | createTableForPrimitiveDataTypes(withTableName: columnName, type: .double(nil)) 33 | } else if let array = attribute as? [Any], let attribute = array.first as? NSNumber, !CFNumberIsFloatType(attribute as CFNumber) { 34 | createTableForPrimitiveDataTypes(withTableName: columnName, type: .integer(nil)) 35 | } else if let array = attribute as? [Any], let _ = array.first as? Double { 36 | createTableForPrimitiveDataTypes(withTableName: columnName, type: .double(nil)) 37 | } else if let array = attribute as? [Any], let _ = array.first as? Float { 38 | createTableForPrimitiveDataTypes(withTableName: columnName, type: .double(nil)) 39 | } else { 40 | createTableForPrimitiveDataTypes(withTableName: columnName) 41 | } 42 | } 43 | let concreteType = object.types?[columnName] 44 | if attribute is Array { 45 | createTableStatement.addColumn(with: columnName, type: .text(nil)) 46 | } else { 47 | switch concreteType { 48 | case .blob: 49 | createTableStatement.addColumn(with: columnName, type: .blob(nil)) 50 | case .double: 51 | createTableStatement.addColumn(with: columnName, type: .double(nil)) 52 | case .integer: 53 | createTableStatement.addColumn(with: columnName, type: .integer(nil)) 54 | default: 55 | createTableStatement.addColumn(with: columnName, type: .text(nil)) 56 | } 57 | } 58 | // if case .blob = concreteType { 59 | // createTableStatement.addColumn(with: columnName, type: .blob) 60 | // } else if let attribute = attribute as? NSNumber, !CFNumberIsFloatType(attribute as CFNumber) { 61 | // createTableStatement.addColumn(with: columnName, type: .integer) 62 | // } else if attribute is Double || attribute is Float || attribute is Date { 63 | // createTableStatement.addColumn(with: columnName, type: .double) 64 | // } else { 65 | // createTableStatement.addColumn(with: columnName, type: .text) 66 | // } 67 | if columnName == "index" { 68 | throw SundeedQLiteError.cantUseNameIndex(tableName: object.tableName) 69 | } 70 | } 71 | if objects[Sundeed.shared.primaryKey] != nil { 72 | createTableStatement.withPrimaryKey() 73 | } 74 | let statement: String? = createTableStatement.build() 75 | SundeedQLiteConnection.pool.execute(query: statement, 76 | parameters: nil) 77 | Sundeed.shared.tables.append(object.tableName) 78 | } 79 | } 80 | /** Try to create table for primitive data types if not already exists */ 81 | func createTableForPrimitiveDataTypes(withTableName tableName: String, 82 | type: ParameterType = .text(nil)) { 83 | if !Sundeed.shared.tables.contains(tableName) { 84 | let createTableStatement = StatementBuilder() 85 | .createTableStatement(tableName: tableName) 86 | .addColumn(with: Sundeed.shared.valueColumnName, type: type) 87 | .build() 88 | SundeedQLiteConnection.pool.execute(query: createTableStatement, parameters: nil) 89 | Sundeed.shared.tables.append(tableName) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Library/Main/Statements/SelectStatement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedSelectStatement.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/10/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SelectStatement: Statement { 12 | let queue = DispatchQueue(label: "thread-safe-select-statement", attributes: .concurrent) 13 | private var tableName: String 14 | private var filters: [SundeedExpression] = [] 15 | private var caseInSensitive: Bool = false 16 | private var orderByColumnName: String? 17 | private var isOrdered: Bool = false 18 | private var ascending: Bool = true 19 | private var limit: Int? 20 | private var skip: Int? 21 | 22 | init(with tableName: String) { 23 | self.tableName = tableName 24 | } 25 | @discardableResult 26 | func isCaseInsensitive(_ isCaseInsensitive: Bool) -> Self { 27 | queue.sync { 28 | self.caseInSensitive = isCaseInsensitive 29 | return self 30 | } 31 | } 32 | @discardableResult 33 | func isAscending(_ isAscending: Bool) -> Self { 34 | queue.sync { 35 | self.ascending = isAscending 36 | return self 37 | } 38 | } 39 | @discardableResult 40 | func isOrdered(_ isOrdered: Bool) -> Self { 41 | queue.sync { 42 | self.isOrdered = isOrdered 43 | return self 44 | } 45 | } 46 | @discardableResult 47 | func orderBy(columnName: String?) -> Self { 48 | queue.sync { 49 | orderByColumnName = columnName 50 | return self 51 | } 52 | } 53 | @discardableResult 54 | func limit(_ limit: Int?) -> Self { 55 | queue.sync { 56 | self.limit = limit 57 | return self 58 | } 59 | } 60 | @discardableResult 61 | func skip(_ skip: Int?) -> Self { 62 | queue.sync { 63 | self.skip = skip 64 | return self 65 | } 66 | } 67 | @discardableResult 68 | func withFilters(_ filters: SundeedExpression?...) -> Self { 69 | queue.sync { 70 | self.filters = filters.compactMap({$0}) 71 | return self 72 | } 73 | } 74 | @discardableResult 75 | func withFilters(_ filters: [SundeedExpression?]) -> Self { 76 | queue.sync { 77 | self.filters = filters.compactMap({$0}) 78 | return self 79 | } 80 | } 81 | @discardableResult 82 | func excludeIfIsForeign(_ exclude: Bool) -> Self { 83 | queue.sync { 84 | if exclude { 85 | self.filters = self.filters + [SundeedColumn(Sundeed.shared.foreignKey) == nil] 86 | } 87 | return self 88 | } 89 | } 90 | func build() -> String? { 91 | queue.sync { 92 | var statement = "SELECT * FROM \(tableName)" 93 | addFilters(toStatement: &statement) 94 | addOrderBy(toStatement: &statement) 95 | addLimit(toStatement: &statement) 96 | addSkip(toStatement: &statement) 97 | statement.append(";") 98 | SundeedLogger.debug("Select Statement: \(statement)") 99 | return statement 100 | } 101 | } 102 | private func addFilters(toStatement statement: inout String) { 103 | queue.sync { 104 | if !filters.isEmpty { 105 | statement += " WHERE " 106 | for (index, filter) in filters.enumerated() { 107 | let whereStatement = filter.toQuery() 108 | statement.append(whereStatement) 109 | addSeparatorIfNeeded(separator: " AND ", 110 | forStatement: &statement, 111 | needed: isLastIndex(index: index, in: filters)) 112 | } 113 | } 114 | } 115 | } 116 | private func addOrderBy(toStatement statement: inout String) { 117 | queue.sync { 118 | statement.append(" ORDER BY") 119 | if isOrdered, 120 | let orderByColumnName = orderByColumnName { 121 | let condition = " \(orderByColumnName)" 122 | statement.append(condition) 123 | } else { 124 | statement.append(" SUNDEED_OFFLINE_ID") 125 | } 126 | addCaseInsensitive(toStatement: &statement) 127 | let sorting = ascending ? " ASC" : " DESC" 128 | statement.append(sorting) 129 | } 130 | } 131 | private func addLimit(toStatement statement: inout String) { 132 | queue.sync { 133 | if let limit { 134 | statement.append(" LIMIT \(limit)") 135 | } 136 | } 137 | } 138 | private func addSkip(toStatement statement: inout String) { 139 | queue.sync { 140 | if let skip { 141 | if limit == nil { 142 | statement.append(" LIMIT -1") 143 | } 144 | statement.append(" OFFSET \(skip)") 145 | } 146 | } 147 | } 148 | private func addCaseInsensitive(toStatement statement: inout String) { 149 | queue.sync { 150 | if caseInSensitive { 151 | statement.append(" COLLATE NOCASE") 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Updates/UpdateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class UpdateTests: XCTestCase { 13 | var employer: EmployerForTesting = EmployerForTesting() 14 | 15 | override func setUp() { 16 | employer.fillData() 17 | } 18 | override func tearDown() async throws { 19 | try await employer.delete(deleteSubObjects: true) 20 | } 21 | 22 | func testGlobalUpdate() async { 23 | await employer.save() 24 | do { 25 | try await EmployerForTesting.update(changes: SundeedColumn("optionalString") <~ "test", 26 | withFilter: SundeedColumn("string") == "string") 27 | let allEmployers = await EmployerForTesting.retrieve(withFilter: SundeedColumn("string") == "string") 28 | guard let employer = allEmployers.first else { 29 | XCTFail("Couldn't Retrieve From Database") 30 | return 31 | } 32 | XCTAssert(employer.optionalString == "test") 33 | } catch { 34 | XCTFail("Couldn't update Global Employer Table \(error)") 35 | } 36 | } 37 | 38 | 39 | func testUpdate() async { 40 | await employer.save() 41 | do { 42 | self.employer.optionalString = "test" 43 | self.employer.object.firstName = "testtt" 44 | self.employer.arrayOfStrings.append("Hello") 45 | self.employer.arrayOfOptionalImages.append(UIImage(named: "1")) 46 | self.employer.arrayOfImages.append(UIImage(named: "5")!) 47 | self.employer.nilImage = UIImage(named: "3")! 48 | self.employer.nilDate = Date() 49 | let employee = EmployeeForTesting(id: "LLLLLLL", seniorID: "TestID1", juniorID: "TestID2") 50 | self.employer.arrayOfObjects.append(employee) 51 | try await self.employer.update(columns: SundeedColumn("optionalString"), 52 | SundeedColumn("object"), 53 | SundeedColumn("arrayOfStrings"), 54 | SundeedColumn("arrayOfOptionalImages"), 55 | SundeedColumn("arrayOfImages"), 56 | SundeedColumn("nilImage"), 57 | SundeedColumn("nilDate"), 58 | SundeedColumn("arrayOfObjects") 59 | ) 60 | let allEmployers = await EmployerForTesting.retrieve() 61 | guard let employer1 = allEmployers.first else { 62 | XCTFail("Couldn't Retrieve From Database") 63 | return 64 | } 65 | XCTAssert(employer1.optionalString == "test") 66 | XCTAssert(employer1.object.firstName == "testtt") 67 | XCTAssert(employer1.arrayOfStrings.contains("Hello")) 68 | XCTAssert(employer1.arrayOfOptionalImages.count == 2) 69 | XCTAssertEqual(employer1.arrayOfOptionalImages.first??.jpegData(compressionQuality: 1)?.description, 70 | UIImage(named: "5")?.jpegData(compressionQuality: 1)?.description) 71 | XCTAssertEqual(employer1.arrayOfOptionalImages[1]?.jpegData(compressionQuality: 1)?.description, 72 | UIImage(named: "1")?.jpegData(compressionQuality: 1)?.description) 73 | XCTAssert(employer1.arrayOfImages.count == 3) 74 | XCTAssertEqual(employer1.arrayOfImages.first?.jpegData(compressionQuality: 1)?.description, 75 | UIImage(named: "3")?.jpegData(compressionQuality: 1)?.description) 76 | XCTAssertEqual(employer1.arrayOfImages[1].jpegData(compressionQuality: 1)?.description, 77 | UIImage(named: "4")?.jpegData(compressionQuality: 1)?.description) 78 | XCTAssertEqual(employer1.arrayOfImages[2].jpegData(compressionQuality: 1)?.description, 79 | UIImage(named: "5")?.jpegData(compressionQuality: 1)?.description) 80 | XCTAssertNotNil(employer1.nilImage) 81 | XCTAssertEqual(employer1.nilImage?.jpegData(compressionQuality: 1)?.description, 82 | UIImage(named: "3")?.jpegData(compressionQuality: 1)?.description) 83 | XCTAssertNotNil(employer1.nilDate) 84 | XCTAssert(employer1.arrayOfObjects.contains(where: {$0.id == "LLLLLLL" })) 85 | } catch { 86 | guard let sundeedError = error as? SundeedQLiteError else { 87 | XCTFail("Wrong Error") 88 | return 89 | } 90 | XCTFail(sundeedError.description) 91 | } 92 | } 93 | 94 | func testWrongColumnNameUpdate() async { 95 | do { 96 | try await self.employer.update(columns: SundeedColumn("wrongColumnName")) 97 | } catch { 98 | guard let sundeedError = error as? SundeedQLiteError else { 99 | XCTFail("Wrong Error") 100 | return 101 | } 102 | XCTAssert(sundeedError.description == SundeedQLiteError.noColumnWithThisName(tableName: "EmployerForTesting", 103 | columnName: "wrongColumnName").description) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Filters/SundeedExpressionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedExpressionTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | 6 | import XCTest 7 | @testable import SundeedQLiteLibrary 8 | 9 | final class SundeedExpressionTests: XCTestCase { 10 | func testEqualStringWithoutQuote() { 11 | let expr = SundeedColumn("name") == "alice" 12 | XCTAssertEqual(expr.toQuery(), "name = 'alice'") 13 | } 14 | 15 | func testEqualStringWithSingleQuote() { 16 | let expr = SundeedColumn("name") == "a'b" 17 | XCTAssertEqual(expr.toQuery(), "name = \"a'b\"") 18 | } 19 | 20 | func testNilPositiveAndNegative() { 21 | let isNull = SundeedColumn("note") == nil 22 | XCTAssertEqual(isNull.toQuery(), "note IS NULL") 23 | let notNull = SundeedColumn("note") != nil 24 | XCTAssertEqual(notNull.toQuery(), "note IS NOT NULL") 25 | } 26 | 27 | func testNumericComparisons() { 28 | XCTAssertEqual((SundeedColumn("age") >= 18).toQuery(), "age >= 18") 29 | XCTAssertEqual((SundeedColumn("age") > 18).toQuery(), "age > 18") 30 | XCTAssertEqual((SundeedColumn("age") <= 18).toQuery(), "age <= 18") 31 | XCTAssertEqual((SundeedColumn("age") < 18).toQuery(), "age < 18") 32 | XCTAssertEqual((SundeedColumn("age") == 18).toQuery(), "age = 18") 33 | XCTAssertEqual((SundeedColumn("age") != 18).toQuery(), "age <> 18") 34 | } 35 | 36 | func testGroupingAndOr() { 37 | let a = SundeedColumn("a") == 1 38 | let b = SundeedColumn("b") == 2 39 | let c = SundeedColumn("c") == 3 40 | let andGroup = a && b 41 | let orGroup = andGroup || c 42 | XCTAssertEqual(andGroup.toQuery(), "(a = 1) AND (b = 2)") 43 | XCTAssertEqual(orGroup.toQuery(), "((a = 1) AND (b = 2)) OR (c = 3)") 44 | } 45 | 46 | func testStringNotEqual() { 47 | XCTAssertEqual((SundeedColumn("name") != "alice").toQuery(), "name <> 'alice'") 48 | XCTAssertEqual((SundeedColumn("name") != nil).toQuery(), "name IS NOT NULL") 49 | } 50 | 51 | func testBoolOperators() { 52 | XCTAssertEqual((SundeedColumn("active") == true).toQuery(), "active = true") 53 | XCTAssertEqual((SundeedColumn("active") == false).toQuery(), "active = false") 54 | XCTAssertEqual((SundeedColumn("active") != true).toQuery(), "active <> true") 55 | XCTAssertEqual((SundeedColumn("active") != false).toQuery(), "active <> false") 56 | } 57 | 58 | func testDoubleOperators() { 59 | XCTAssertEqual((SundeedColumn("price") == 19.99).toQuery(), "price = 19.99") 60 | XCTAssertEqual((SundeedColumn("price") != 19.99).toQuery(), "price <> 19.99") 61 | XCTAssertEqual((SundeedColumn("price") >= 19.99).toQuery(), "price >= 19.99") 62 | XCTAssertEqual((SundeedColumn("price") > 19.99).toQuery(), "price > 19.99") 63 | XCTAssertEqual((SundeedColumn("price") <= 19.99).toQuery(), "price <= 19.99") 64 | XCTAssertEqual((SundeedColumn("price") < 19.99).toQuery(), "price < 19.99") 65 | } 66 | 67 | func testFloatOperators() { 68 | XCTAssertEqual((SundeedColumn("score") == 3.14 as Float).toQuery(), "score = 3.14") 69 | XCTAssertEqual((SundeedColumn("score") != 3.14 as Float).toQuery(), "score <> 3.14") 70 | XCTAssertEqual((SundeedColumn("score") >= 3.14 as Float).toQuery(), "score >= 3.14") 71 | XCTAssertEqual((SundeedColumn("score") > 3.14 as Float).toQuery(), "score > 3.14") 72 | XCTAssertEqual((SundeedColumn("score") <= 3.14 as Float).toQuery(), "score <= 3.14") 73 | XCTAssertEqual((SundeedColumn("score") < 3.14 as Float).toQuery(), "score <= 3.14") 74 | } 75 | 76 | func testDateOperators() { 77 | let date = Date(timeIntervalSince1970: 1000) 78 | let expectedTime = 1000.0 * 1000.0 79 | 80 | XCTAssertEqual((SundeedColumn("created") == date).toQuery(), "created = \(expectedTime)") 81 | XCTAssertEqual((SundeedColumn("created") != date).toQuery(), "created <> \(expectedTime)") 82 | XCTAssertEqual((SundeedColumn("created") >= date).toQuery(), "created >= \(expectedTime)") 83 | XCTAssertEqual((SundeedColumn("created") > date).toQuery(), "created > \(expectedTime)") 84 | XCTAssertEqual((SundeedColumn("created") <= date).toQuery(), "created <= \(expectedTime)") 85 | XCTAssertEqual((SundeedColumn("created") < date).toQuery(), "created < \(expectedTime)") 86 | } 87 | 88 | func testGroupFlatteningSameOperator() { 89 | let a = SundeedColumn("a") == 1 90 | let b = SundeedColumn("b") == 2 91 | let c = SundeedColumn("c") == 3 92 | let d = SundeedColumn("d") == 4 93 | 94 | let group1 = a && b 95 | let group2 = c && d 96 | let combined = group1 && group2 97 | 98 | XCTAssertEqual(combined.toQuery(), "(a = 1) AND (b = 2) AND (c = 3) AND (d = 4)") 99 | } 100 | 101 | func testGroupFlatteningDifferentOperators() { 102 | let a = SundeedColumn("a") == 1 103 | let b = SundeedColumn("b") == 2 104 | let c = SundeedColumn("c") == 3 105 | 106 | let andGroup = a && b 107 | let orGroup = andGroup || c 108 | 109 | XCTAssertEqual(orGroup.toQuery(), "((a = 1) AND (b = 2)) OR (c = 3)") 110 | } 111 | 112 | func testSingleExpressionGroup() { 113 | let a = SundeedColumn("a") == 1 114 | let singleGroup = SundeedExpression([a], logicalOperator: .and) 115 | XCTAssertEqual(singleGroup.toQuery(), "a = 1") 116 | } 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | noursandid@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Listeners/Listeners.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Listeners.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class Listeners: XCTestCase { 13 | var employer: EmployerForTesting? = EmployerForTesting() 14 | 15 | override func setUp() { 16 | employer?.fillData() 17 | } 18 | 19 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 20 | Task { 21 | await EmployerForTesting.delete() 22 | completion(nil) 23 | } 24 | } 25 | 26 | func testSpecificOnAllEventsListener() { 27 | let expectation = XCTestExpectation(description: "SpecificOnAllEventsListener") 28 | let listener = self.employer?.onAllEvents({ (object) in 29 | XCTAssertEqual(object.string, "string") 30 | expectation.fulfill() 31 | }) 32 | Task { 33 | await EmployerForTesting.delete() 34 | await self.employer?.save() 35 | } 36 | wait(for: [expectation], timeout: 5) 37 | listener?.stop() 38 | } 39 | 40 | func testSpecificOnSaveListener() { 41 | let expectation = XCTestExpectation(description: "SpecificOnSaveListener") 42 | let listener = self.employer?.onSaveEvents({ (object) in 43 | XCTAssertEqual(object.string, "string") 44 | expectation.fulfill() 45 | }) 46 | Task { 47 | await EmployerForTesting.delete() 48 | await self.employer?.save() 49 | } 50 | wait(for: [expectation], timeout: 5) 51 | listener?.stop() 52 | } 53 | 54 | func testSpecificOnUpdateListener() { 55 | let expectation = XCTestExpectation(description: "SpecificOnUpdateListener") 56 | let listener = self.employer?.onUpdateEvents({ (object) in 57 | XCTAssertEqual(object.string, "test") 58 | expectation.fulfill() 59 | }) 60 | Task { 61 | await self.employer?.save() 62 | self.employer?.string = "test" 63 | try? await self.employer?.update(columns: SundeedColumn("string")) 64 | } 65 | wait(for: [expectation], timeout: 5) 66 | listener?.stop() 67 | } 68 | 69 | func testSpecificOnRetrieveListener() { 70 | let expectation = XCTestExpectation(description: "SpecificOnRetrieveListener") 71 | let listener = self.employer?.onRetrieveEvents({ (object) in 72 | XCTAssertEqual(object.string, "string") 73 | expectation.fulfill() 74 | }) 75 | Task { 76 | await EmployerForTesting.delete() 77 | await self.employer?.save() 78 | let _ = await EmployerForTesting.retrieve() 79 | } 80 | wait(for: [expectation], timeout: 5) 81 | listener?.stop() 82 | } 83 | 84 | func testSpecificOnDeleteListener() { 85 | let expectation = XCTestExpectation(description: "SpecificOnDeleteListener") 86 | let listener = self.employer?.onDeleteEvents({ (object) in 87 | XCTAssertEqual(object.string, "string") 88 | expectation.fulfill() 89 | }) 90 | Task { 91 | try? await employer?.delete() 92 | } 93 | wait(for: [expectation], timeout: 5) 94 | listener?.stop() 95 | } 96 | 97 | 98 | func testGlobalOnAllEventsListener() { 99 | let expectation = XCTestExpectation(description: "GlobalOnAllEventsListener") 100 | let listener = EmployerForTesting.onAllEvents({ (object) in 101 | XCTAssertEqual(object.string, "string") 102 | expectation.fulfill() 103 | }) 104 | Task { 105 | await EmployerForTesting.delete() 106 | await self.employer?.save() 107 | } 108 | wait(for: [expectation], timeout: 5) 109 | listener.stop() 110 | } 111 | 112 | func testGlobalOnSaveListener() { 113 | let expectation = XCTestExpectation(description: "GlobalOnSaveListener") 114 | let listener = EmployerForTesting.onSaveEvents({ (object) in 115 | XCTAssertEqual(object.string, "string") 116 | expectation.fulfill() 117 | }) 118 | Task { 119 | await EmployerForTesting.delete() 120 | await self.employer?.save() 121 | } 122 | wait(for: [expectation], timeout: 5) 123 | listener.stop() 124 | } 125 | 126 | func testGlobalOnUpdateListener() { 127 | let expectation = XCTestExpectation(description: "GlobalOnUpdateListener") 128 | let listener = EmployerForTesting.onUpdateEvents({ (object) in 129 | XCTAssertEqual(object.string, "test") 130 | expectation.fulfill() 131 | }) 132 | Task { 133 | await employer?.save() 134 | self.employer?.string = "test" 135 | try? await self.employer?.update(columns: SundeedColumn("string")) 136 | } 137 | wait(for: [expectation], timeout: 5) 138 | listener.stop() 139 | } 140 | 141 | func testGlobalOnRetrieveListener() { 142 | let expectation = XCTestExpectation(description: "GlobalOnRetrieveListener") 143 | let listener = EmployerForTesting.onRetrieveEvents({ (object) in 144 | XCTAssertEqual(object.string, "string") 145 | expectation.fulfill() 146 | }) 147 | Task { 148 | await EmployerForTesting.delete() 149 | await self.employer?.save() 150 | let _ = await EmployerForTesting.retrieve() 151 | } 152 | wait(for: [expectation], timeout: 5) 153 | listener.stop() 154 | } 155 | 156 | func testGlobalOnDeleteListener() { 157 | let expectation = XCTestExpectation(description: "GlobalOnDeleteListener") 158 | let listener = EmployerForTesting.onDeleteEvents({ (object) in 159 | XCTAssertEqual(object.string, "string") 160 | expectation.fulfill() 161 | }) 162 | Task { 163 | try? await self.employer?.delete() 164 | } 165 | wait(for: [expectation], timeout: 5) 166 | listener.stop() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Operator/MandatoryClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MandatoryClasses.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import SundeedQLiteLibrary 11 | 12 | class ClassWithMandatoryOptionalString: @unchecked Sendable, SundeedQLiter { 13 | var id: String = "qwe1" 14 | var mandatory: String? 15 | required init() {} 16 | 17 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 18 | id <~> map["id"]+ 19 | mandatory <*> map["mandatory"] 20 | } 21 | } 22 | 23 | class ClassWithMandatoryOptionalInt: @unchecked Sendable, SundeedQLiter { 24 | var id: String = "qwe2" 25 | var mandatory: Int? 26 | required init() {} 27 | 28 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 29 | id <~> map["id"]+ 30 | mandatory <*> map["mandatory"] 31 | } 32 | } 33 | 34 | class ClassWithMandatoryOptionalDate: @unchecked Sendable, SundeedQLiter { 35 | var id: String = "qwe3" 36 | var mandatory: Date? 37 | required init() {} 38 | 39 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 40 | id <~> map["id"]+ 41 | mandatory <*> map["mandatory"] 42 | } 43 | } 44 | 45 | class ClassWithMandatoryOptionalImage: @unchecked Sendable, SundeedQLiter { 46 | var id: String = "qwe4" 47 | var mandatory: UIImage? 48 | required init() {} 49 | 50 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 51 | id <~> map["id"]+ 52 | mandatory <*> map["mandatory"] 53 | } 54 | } 55 | 56 | class ClassWithMandatoryOptionalDouble: @unchecked Sendable, SundeedQLiter { 57 | var id: String = "qwe5" 58 | var mandatory: Double? 59 | required init() {} 60 | 61 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 62 | id <~> map["id"]+ 63 | mandatory <*> map["mandatory"] 64 | } 65 | } 66 | 67 | class ClassWithMandatoryOptionalFloat: @unchecked Sendable, SundeedQLiter { 68 | var id: String = "qwe6" 69 | var mandatory: Float? 70 | required init() {} 71 | 72 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 73 | id <~> map["id"]+ 74 | mandatory <*> map["mandatory"] 75 | } 76 | } 77 | 78 | class ClassWithMandatoryOptionalArrayOfFloats: @unchecked Sendable, SundeedQLiter { 79 | var id: String = "qwe7" 80 | var mandatory: [Float]? 81 | required init() {} 82 | 83 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 84 | id <~> map["id"]+ 85 | mandatory <*> map["mandatory"] 86 | } 87 | } 88 | 89 | class ClassWithMandatoryOptionalArrayOfOptionalFloats: @unchecked Sendable, SundeedQLiter { 90 | var id: String = "qwe8" 91 | var mandatory: [Float?]? 92 | required init() {} 93 | 94 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 95 | id <~> map["id"]+ 96 | mandatory <*> map["mandatory"] 97 | } 98 | } 99 | 100 | class ClassWithMandatoryOptionalArrayOfOptionalDoubles: @unchecked Sendable, SundeedQLiter { 101 | var id: String = "qwe9" 102 | var mandatory: [Double?]? 103 | required init() {} 104 | 105 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 106 | id <~> map["id"]+ 107 | mandatory <*> map["mandatory"] 108 | } 109 | } 110 | 111 | class ClassWithMandatoryOptionalArrayOfDoubles: @unchecked Sendable, SundeedQLiter { 112 | var id: String = "qwe10" 113 | var mandatory: [Double]? 114 | required init() {} 115 | 116 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 117 | id <~> map["id"]+ 118 | mandatory <*> map["mandatory"] 119 | } 120 | } 121 | 122 | class ClassWithMandatoryOptionalArrayOfOptionalInts: @unchecked Sendable, SundeedQLiter { 123 | var id: String = "qwe11" 124 | var mandatory: [Int?]? 125 | required init() {} 126 | 127 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 128 | id <~> map["id"]+ 129 | mandatory <*> map["mandatory"] 130 | } 131 | } 132 | 133 | class ClassWithMandatoryOptionalArrayOfInts: @unchecked Sendable, SundeedQLiter { 134 | var id: String = "qwe12" 135 | var mandatory: [Int]? 136 | required init() {} 137 | 138 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 139 | id <~> map["id"]+ 140 | mandatory <*> map["mandatory"] 141 | } 142 | } 143 | 144 | 145 | 146 | class ClassWithMandatoryOptionalArrayOfImages: @unchecked Sendable, SundeedQLiter { 147 | var id: String = "qwe13" 148 | var mandatory: [UIImage]? 149 | required init() {} 150 | 151 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 152 | id <~> map["id"]+ 153 | mandatory <*> map["mandatory"] 154 | } 155 | } 156 | 157 | class ClassWithMandatoryOptionalArrayOfOptionalImages: @unchecked Sendable, SundeedQLiter { 158 | var id: String = "qwe14" 159 | var mandatory: [UIImage?]? 160 | required init() {} 161 | 162 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 163 | id <~> map["id"]+ 164 | mandatory <*> map["mandatory"] 165 | } 166 | } 167 | 168 | class ClassWithMandatoryOptionalArrayOfStrings: @unchecked Sendable, SundeedQLiter { 169 | var id: String = "qwe15" 170 | var mandatory: [String]? 171 | required init() {} 172 | 173 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 174 | id <~> map["id"]+ 175 | mandatory <*> map["mandatory"] 176 | } 177 | } 178 | 179 | class ClassWithMandatoryOptionalArrayOfOptionalStrings: @unchecked Sendable, SundeedQLiter { 180 | var id: String = "qwe16" 181 | var mandatory: [String?]? 182 | required init() {} 183 | 184 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 185 | id <~> map["id"]+ 186 | mandatory <*> map["mandatory"] 187 | } 188 | } 189 | 190 | class ClassWithMandatoryOptionalObjects: @unchecked Sendable, SundeedQLiter { 191 | var id: String = "qwe17" 192 | var mandatory: MandatoryClass? 193 | required init() {} 194 | 195 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 196 | id <~> map["id"]+ 197 | mandatory <*> map["mandatory"] 198 | } 199 | } 200 | 201 | class ClassWithMandatoryOptionalArrayOfObjects: @unchecked Sendable, SundeedQLiter { 202 | var id: String = "qwe18" 203 | var mandatory: [MandatoryClass]? 204 | required init() {} 205 | 206 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 207 | id <~> map["id"]+ 208 | mandatory <*> map["mandatory"] 209 | } 210 | } 211 | 212 | class ClassWithMandatoryOptionalArrayOfOptionalObjects: @unchecked Sendable, SundeedQLiter { 213 | var id: String = "qwe19" 214 | var mandatory: [MandatoryClass?]? 215 | required init() {} 216 | 217 | func sundeedQLiterMapping(map: SundeedQLiteMap) { 218 | id <~> map["id"]+ 219 | mandatory <*> map["mandatory"] 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Operations/RetrieveWithSorting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetrieveWithSorting.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 02/10/2025. 6 | // Copyright © 2025 LUMBERCODE. All rights reserved. 7 | // 8 | import XCTest 9 | import SundeedQLiteLibrary 10 | 11 | class RetrieveWithSorting: XCTestCase { 12 | var employer: EmployerForTesting? = EmployerForTesting() 13 | 14 | override func setUp() { 15 | employer?.fillData() 16 | } 17 | override func tearDown() async throws { 18 | await EmployerForTesting.delete() 19 | await EmployeeForTesting.delete() 20 | } 21 | func testRetrieveWithSortingIntAsc() async { 22 | let employee1 = EmployeeForTesting(id: "employee1", seniorID: "senior1", juniorID: "junior1") 23 | employee1.integer = 1 24 | employee1.firstName = "employee1" 25 | await employee1.save() 26 | let employee2 = EmployeeForTesting(id: "employee2", seniorID: "senior2", juniorID: "junior2") 27 | employee1.id = "employee2" 28 | employee2.integer = 2 29 | employee1.firstName = "employee2" 30 | await employee2.save() 31 | let allEmployees = await EmployeeForTesting.retrieve() 32 | XCTAssertEqual(allEmployees.count, 2) 33 | let firstEmployee = allEmployees[0] 34 | let secondEmployee = allEmployees[1] 35 | XCTAssertEqual(firstEmployee.integer, 1) 36 | XCTAssertEqual(secondEmployee.integer, 2) 37 | _ = try? await employee1.delete(deleteSubObjects: true) 38 | _ = try? await employee2.delete(deleteSubObjects: true) 39 | } 40 | 41 | func testRetrieveWithSortingIntDesc() async { 42 | 43 | let employer2 = EmployerForTesting() 44 | employer2.fillData() 45 | employer2.string = "stri" 46 | employer2.integer = 3 47 | await [employer!, employer2].save() 48 | let allEmployers = await EmployerForTesting.retrieve() 49 | XCTAssertEqual(allEmployers.count, 2) 50 | let firstEmployer = allEmployers[0] 51 | let secondEmployer = allEmployers[1] 52 | XCTAssertEqual(firstEmployer.integer, 3) 53 | secondEmployer.check() 54 | _ = try? await self.employer?.delete(deleteSubObjects: true) 55 | _ = try? await employer2.delete(deleteSubObjects: true) 56 | } 57 | 58 | func testRetrieveWithSortingStringAsc() async { 59 | 60 | let employer2 = EmployerForTesting() 61 | employer2.fillData() 62 | employer2.string = "string2" 63 | employer2.integer = 2 64 | await [employer!, employer2].save() 65 | let allEmployers = await EmployerForTesting.retrieve(orderBy: SundeedColumn("string"), 66 | ascending: true) 67 | XCTAssertEqual(allEmployers.count, 2) 68 | let firstEmployer = allEmployers[0] 69 | let secondEmployer = allEmployers[1] 70 | firstEmployer.check() 71 | XCTAssertEqual(secondEmployer.integer, 2) 72 | _ = try? await self.employer?.delete(deleteSubObjects: true) 73 | _ = try? await employer2.delete(deleteSubObjects: true) 74 | } 75 | 76 | func testRetrieveWithSortingStringDesc() async { 77 | 78 | let employer2 = EmployerForTesting() 79 | employer2.fillData() 80 | employer2.string = "string3" 81 | employer2.integer = 3 82 | await [employer!, employer2].save() 83 | let allEmployers = await EmployerForTesting 84 | .retrieve(orderBy: SundeedColumn("string"), 85 | ascending: false) 86 | XCTAssertEqual(allEmployers.count, 2) 87 | guard allEmployers.count == 2 else { 88 | XCTFail() 89 | return 90 | } 91 | let firstEmployer = allEmployers[0] 92 | let secondEmployer = allEmployers[1] 93 | XCTAssertEqual(firstEmployer.integer, 3) 94 | secondEmployer.check() 95 | _ = try? await self.employer?.delete(deleteSubObjects: true) 96 | _ = try? await employer2.delete(deleteSubObjects: true) 97 | } 98 | 99 | func testRetrieveWithSortingDateAsc() async { 100 | 101 | let employer2 = EmployerForTesting() 102 | employer2.fillData() 103 | employer2.string = "string2" 104 | employer2.integer = 2 105 | employer2.date = Date().addingTimeInterval(500) 106 | await [employer!, employer2].save() 107 | let allEmployers = await EmployerForTesting.retrieve(orderBy: SundeedColumn("date"), 108 | ascending: true) 109 | XCTAssertEqual(allEmployers.count, 2) 110 | let firstEmployer = allEmployers[0] 111 | let secondEmployer = allEmployers[1] 112 | firstEmployer.check() 113 | XCTAssertEqual(secondEmployer.integer, 2) 114 | _ = try? await self.employer?.delete(deleteSubObjects: true) 115 | _ = try? await employer2.delete(deleteSubObjects: true) 116 | } 117 | 118 | func testRetrieveWithSortingDateDesc() async { 119 | 120 | let employer2 = EmployerForTesting() 121 | employer2.fillData() 122 | employer2.string = "string3" 123 | employer2.integer = 3 124 | employer2.date = Date().addingTimeInterval(500) 125 | await [employer!, employer2].save() 126 | let allEmployers = await EmployerForTesting.retrieve(orderBy: SundeedColumn("date"), 127 | ascending: false) 128 | XCTAssertEqual(allEmployers.count, 2) 129 | let firstEmployer = allEmployers[0] 130 | let secondEmployer = allEmployers[1] 131 | XCTAssertEqual(firstEmployer.integer, 3) 132 | secondEmployer.check() 133 | _ = try? await self.employer?.delete(deleteSubObjects: true) 134 | _ = try? await employer2.delete(deleteSubObjects: true) 135 | 136 | } 137 | 138 | func testRetrieveWithSortingEnumAsc() async { 139 | 140 | let employer2 = EmployerForTesting() 141 | employer2.fillData() 142 | employer2.string = "string2" 143 | employer2.integer = 2 144 | employer2.type = .ceo 145 | await [employer!, employer2].save() 146 | let allEmployers = await EmployerForTesting.retrieve(orderBy: SundeedColumn("type"), 147 | ascending: true) 148 | XCTAssertEqual(allEmployers.count, 2) 149 | let firstEmployer = allEmployers[0] 150 | let secondEmployer = allEmployers[1] 151 | secondEmployer.check() 152 | XCTAssertEqual(firstEmployer.integer, 2) 153 | _ = try? await self.employer?.delete(deleteSubObjects: true) 154 | _ = try? await employer2.delete(deleteSubObjects: true) 155 | 156 | } 157 | 158 | func testRetrieveWithSortingEnumDesc() async { 159 | 160 | let employer2 = EmployerForTesting() 161 | employer2.fillData() 162 | employer2.string = "string3" 163 | employer2.integer = 3 164 | employer2.type = .ceo 165 | await [employer!, employer2].save() 166 | let allEmployers = await EmployerForTesting.retrieve(orderBy: SundeedColumn("type"), 167 | ascending: false) 168 | XCTAssertEqual(allEmployers.count, 2) 169 | let firstEmployer = allEmployers[0] 170 | let secondEmployer = allEmployers[1] 171 | firstEmployer.check() 172 | XCTAssertEqual(secondEmployer.integer, 3) 173 | _ = try? await self.employer?.delete(deleteSubObjects: true) 174 | _ = try? await employer2.delete(deleteSubObjects: true) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Library/Main/SundeedExpression.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedExpression.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 30/09/2025. 6 | // Copyright © 2025 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** SundeedColumn("columnName") == "value" */ 12 | public struct SundeedExpression: @unchecked Sendable { 13 | enum LogicalOperator: String { 14 | case and = "AND" 15 | case or = "OR" 16 | } 17 | 18 | enum ComparisonOperator: String { 19 | case equal = "=" 20 | case notEqual = "<>" 21 | case greaterThan = ">" 22 | case greaterThanOrEqual = ">=" 23 | case lessThan = "<" 24 | case lessThanOrEqual = "<=" 25 | } 26 | 27 | private enum Node { 28 | case comparison(key: String, value: Any?, comparison: ComparisonOperator, positive: Bool) 29 | case group(children: [SundeedExpression], op: LogicalOperator) 30 | } 31 | 32 | private let node: Node 33 | 34 | init(_ key: String, _ value: Any?, comparison: ComparisonOperator = .equal, positive: Bool = true) { 35 | self.node = .comparison(key: key, value: value, comparison: comparison, positive: positive) 36 | } 37 | 38 | // MARK: - Group initializer (combine other expressions with a logical operator) 39 | init(_ expressions: [SundeedExpression], logicalOperator: LogicalOperator = .and) { 40 | var flattened: [SundeedExpression] = [] 41 | for expr in expressions { 42 | if case .group(let children, let op) = expr.node, op == logicalOperator { 43 | // append children's elements (flatten) 44 | flattened.append(contentsOf: children) 45 | } else { 46 | flattened.append(expr) 47 | } 48 | } 49 | if flattened.count == 1 { 50 | self.node = flattened[0].node 51 | } else { 52 | self.node = .group(children: flattened, op: logicalOperator) 53 | } 54 | } 55 | 56 | // MARK: - Produce SQL 57 | func toQuery() -> String { 58 | switch node { 59 | case .comparison(let key, let value, let comparison, let positive): 60 | if let value = value { 61 | let quotations = Statement().getQuotation(forValue: value) 62 | return "\(key) \(comparison.rawValue) \(quotations)\(value)\(quotations)" 63 | } else { 64 | return positive ? "\(key) IS NULL" : "\(key) IS NOT NULL" 65 | } 66 | 67 | case .group(let children, let op): 68 | let parts = children.map { "(\($0.toQuery()))" } 69 | return parts.joined(separator: " \(op.rawValue) ") 70 | } 71 | } 72 | } 73 | 74 | /** Filter in local database */ 75 | public func && (left: SundeedExpression, right: SundeedExpression) -> SundeedExpression { 76 | return SundeedExpression([left, right], logicalOperator: .and) 77 | } 78 | public func || (left: SundeedExpression, right: SundeedExpression) -> SundeedExpression { 79 | return SundeedExpression([left, right], logicalOperator: .or) 80 | } 81 | public func != (left: SundeedColumn, right: String?) -> SundeedExpression { 82 | return SundeedExpression(left.value, right, comparison: .notEqual, positive: false) 83 | } 84 | public func == (left: SundeedColumn, right: String?) -> SundeedExpression { 85 | return SundeedExpression(left.value, right, comparison: .equal) 86 | } 87 | public func != (left: SundeedColumn, right: Bool) -> SundeedExpression { 88 | return SundeedExpression(left.value, right, comparison: .notEqual, positive: false) 89 | } 90 | public func == (left: SundeedColumn, right: Bool) -> SundeedExpression { 91 | return SundeedExpression(left.value, right, comparison: .equal) 92 | } 93 | public func != (left: SundeedColumn, right: Int) -> SundeedExpression { 94 | return SundeedExpression(left.value, right, comparison: .notEqual, positive: false) 95 | } 96 | public func == (left: SundeedColumn, right: Int) -> SundeedExpression { 97 | return SundeedExpression(left.value, right, comparison: .equal) 98 | } 99 | public func >= (left: SundeedColumn, right: Int) -> SundeedExpression { 100 | return SundeedExpression(left.value, right, comparison: .greaterThanOrEqual) 101 | } 102 | public func > (left: SundeedColumn, right: Int) -> SundeedExpression { 103 | return SundeedExpression(left.value, right, comparison: .greaterThan) 104 | } 105 | public func <= (left: SundeedColumn, right: Int) -> SundeedExpression{ 106 | return SundeedExpression(left.value, right, comparison: .lessThanOrEqual) 107 | } 108 | public func < (left: SundeedColumn, right: Int) -> SundeedExpression { 109 | return SundeedExpression(left.value, right, comparison: .lessThan) 110 | } 111 | public func != (left: SundeedColumn, right: Double) -> SundeedExpression { 112 | return SundeedExpression(left.value, right, comparison: .notEqual, positive: false) 113 | } 114 | public func == (left: SundeedColumn, right: Double) -> SundeedExpression { 115 | return SundeedExpression(left.value, right, comparison: .equal) 116 | } 117 | public func >= (left: SundeedColumn, right: Double) -> SundeedExpression { 118 | return SundeedExpression(left.value, right, comparison: .greaterThanOrEqual) 119 | } 120 | public func > (left: SundeedColumn, right: Double) -> SundeedExpression { 121 | return SundeedExpression(left.value, right, comparison: .greaterThan) 122 | } 123 | public func <= (left: SundeedColumn, right: Double) -> SundeedExpression { 124 | return SundeedExpression(left.value, right, comparison: .lessThanOrEqual) 125 | } 126 | public func < (left: SundeedColumn, right: Double) -> SundeedExpression { 127 | return SundeedExpression(left.value, right, comparison: .lessThan) 128 | } 129 | public func != (left: SundeedColumn, right: Float) -> SundeedExpression { 130 | return SundeedExpression(left.value, right, comparison: .notEqual, positive: false) 131 | } 132 | public func == (left: SundeedColumn, right: Float) -> SundeedExpression { 133 | return SundeedExpression(left.value, right, comparison: .equal) 134 | } 135 | public func >= (left: SundeedColumn, right: Float) -> SundeedExpression { 136 | return SundeedExpression(left.value, right, comparison: .greaterThanOrEqual) 137 | } 138 | public func > (left: SundeedColumn, right: Float) -> SundeedExpression { 139 | return SundeedExpression(left.value, right, comparison: .greaterThan) 140 | } 141 | public func <= (left: SundeedColumn, right: Float) -> SundeedExpression { 142 | return SundeedExpression(left.value, right, comparison: .lessThanOrEqual) 143 | } 144 | public func < (left: SundeedColumn, right: Float) -> SundeedExpression { 145 | return SundeedExpression(left.value, right, comparison: .lessThanOrEqual) 146 | } 147 | public func != (left: SundeedColumn, right: Date) -> SundeedExpression { 148 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .notEqual, positive: false) 149 | } 150 | public func == (left: SundeedColumn, right: Date) -> SundeedExpression { 151 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .equal) 152 | } 153 | public func >= (left: SundeedColumn, right: Date) -> SundeedExpression { 154 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .greaterThanOrEqual) 155 | } 156 | public func > (left: SundeedColumn, right: Date) -> SundeedExpression { 157 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .greaterThan) 158 | } 159 | public func <= (left: SundeedColumn, right: Date) -> SundeedExpression { 160 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .lessThanOrEqual) 161 | } 162 | public func < (left: SundeedColumn, right: Date) -> SundeedExpression { 163 | return SundeedExpression(left.value, right.timeIntervalSince1970*1000, comparison: .lessThan) 164 | } 165 | -------------------------------------------------------------------------------- /Library/ClassHandler/Map/SundeedQLiteMap+ArrayMandatory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteMap+ArrayMandatory.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/16/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | infix operator <**> 12 | public func <**> (left: inout [T], right: SundeedQLiteMap) { 13 | if !right.fetchingColumns { 14 | var array: [SundeedQLiter] = [] 15 | if let values = right.currentValue as? NSArray { 16 | for value in values where value is [String: Any] { 17 | if let value = value as? [String: Any] { 18 | let object = T() 19 | let map = SundeedQLiteMap(dictionnary: value) 20 | object.sundeedQLiterMapping(map: map) 21 | if !map.isSafeToAdd && right.isSafeToAdd { 22 | right.isSafeToAdd = false 23 | } 24 | let referencedInstance = SundeedQLiteMap 25 | .getReference(andValue: map[map.primaryKey].currentValue as AnyObject, 26 | andClassName: "\(T.self)") 27 | if let referencedInstance = referencedInstance { 28 | let map = SundeedQLiteMap(dictionnary: value) 29 | referencedInstance.sundeedQLiterMapping(map: map) 30 | array.append(referencedInstance) 31 | } else { 32 | SundeedQLiteMap.addReference(object: object, 33 | andValue: map[map.primaryKey].currentValue as AnyObject, 34 | andClassName: "\(T.self)") 35 | array.append(object) 36 | } 37 | } 38 | } 39 | } 40 | if let array = array as? [T] { 41 | left = array 42 | } 43 | if (array.count == 0) && right.isSafeToAdd { 44 | right.isSafeToAdd = false 45 | } 46 | } else { 47 | if let key = right.key { 48 | right.addColumn(attribute: left, withColumnName: key, type: .text(nil)) 49 | } 50 | } 51 | } 52 | public func <**> (left: inout [T]?, right: SundeedQLiteMap) { 53 | if !right.fetchingColumns { 54 | if let values = right.currentValue as? NSArray { 55 | var array: [SundeedQLiter] = [] 56 | for value in values where value is [String: Any] { 57 | if let value = value as? [String: Any] { 58 | let object = T() 59 | let map = SundeedQLiteMap(dictionnary: value) 60 | object.sundeedQLiterMapping(map: map) 61 | if !map.isSafeToAdd && right.isSafeToAdd { 62 | right.isSafeToAdd = false 63 | } 64 | let referencedInstance = SundeedQLiteMap 65 | .getReference(andValue: map[map.primaryKey].currentValue as AnyObject, 66 | andClassName: "\(T.self)") 67 | if let referencedInstance = referencedInstance { 68 | let map = SundeedQLiteMap(dictionnary: value) 69 | referencedInstance.sundeedQLiterMapping(map: map) 70 | array.append(referencedInstance) 71 | } else { 72 | SundeedQLiteMap.addReference(object: object, 73 | andValue: map[map.primaryKey].currentValue as AnyObject, 74 | andClassName: "\(T.self)") 75 | array.append(object) 76 | } 77 | } 78 | } 79 | left = array as? [T] 80 | if (array.count == 0) && right.isSafeToAdd { 81 | right.isSafeToAdd = false 82 | } 83 | } 84 | if (left == nil) && right.isSafeToAdd { 85 | right.isSafeToAdd = false 86 | } 87 | } else { 88 | if let key = right.key { 89 | right.addColumn(attribute: left, withColumnName: key, type: .text(nil)) 90 | } 91 | } 92 | } 93 | public func <**> (left: inout [T?]?, right: SundeedQLiteMap) { 94 | if !right.fetchingColumns { 95 | if let values = right.currentValue as? NSArray { 96 | var array: [SundeedQLiter] = [] 97 | for value in values where value is [String: Any] { 98 | if let value = value as? [String: Any] { 99 | let object = T() 100 | let map = SundeedQLiteMap(dictionnary: value) 101 | object.sundeedQLiterMapping(map: map) 102 | if !map.isSafeToAdd && right.isSafeToAdd { 103 | right.isSafeToAdd = false 104 | } 105 | let referencedInstance = SundeedQLiteMap 106 | .getReference(andValue: map[map.primaryKey].currentValue as AnyObject, 107 | andClassName: "\(T.self)") 108 | if let referencedInstance = referencedInstance { 109 | let map = SundeedQLiteMap(dictionnary: value) 110 | referencedInstance.sundeedQLiterMapping(map: map) 111 | array.append(referencedInstance) 112 | } else { 113 | SundeedQLiteMap.addReference(object: object, 114 | andValue: map[map.primaryKey].currentValue as AnyObject, 115 | andClassName: "\(T.self)") 116 | array.append(object) 117 | } 118 | } 119 | } 120 | left = array as? [T] 121 | if (array.count == 0) && right.isSafeToAdd { 122 | right.isSafeToAdd = false 123 | } 124 | } 125 | if (left == nil) && right.isSafeToAdd { 126 | right.isSafeToAdd = false 127 | } 128 | } else { 129 | if let key = right.key { 130 | right.addColumn(attribute: left, withColumnName: key, type: .text(nil)) 131 | } 132 | } 133 | } 134 | public func <**> (left: inout [T?], right: SundeedQLiteMap) { 135 | if !right.fetchingColumns { 136 | var array: [SundeedQLiter] = [] 137 | if let values = right.currentValue as? NSArray { 138 | for value in values where value is [String: Any] { 139 | if let value = value as? [String: Any] { 140 | let object = T() 141 | let map = SundeedQLiteMap(dictionnary: value) 142 | object.sundeedQLiterMapping(map: map) 143 | if !map.isSafeToAdd && right.isSafeToAdd { 144 | right.isSafeToAdd = false 145 | } 146 | let referencedInstance = SundeedQLiteMap 147 | .getReference(andValue: map[map.primaryKey].currentValue as AnyObject, 148 | andClassName: "\(T.self)") 149 | if referencedInstance != nil { 150 | let map = SundeedQLiteMap(dictionnary: value) 151 | referencedInstance?.sundeedQLiterMapping(map: map) 152 | array.append(referencedInstance!) 153 | } else { 154 | SundeedQLiteMap.addReference(object: object, 155 | andValue: map[map.primaryKey].currentValue as AnyObject, 156 | andClassName: "\(T.self)") 157 | array.append(object) 158 | } 159 | } 160 | } 161 | if let array = array as? [T] { 162 | left = array 163 | } 164 | } 165 | if (array.count == 0) && right.isSafeToAdd { 166 | right.isSafeToAdd = false 167 | } 168 | } else { 169 | if let key = right.key { 170 | right.addColumn(attribute: left, withColumnName: key, type: .text(nil)) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Operator/MandatoryOperatorTestWithoutData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MandatoryOperatorTest.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import SundeedQLiteLibrary 12 | 13 | class MandatoryOperatorTestWithoutData: XCTestCase { 14 | 15 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 16 | Task { 17 | await ClassWithMandatoryOptionalString.delete() 18 | await ClassWithMandatoryOptionalInt.delete() 19 | await ClassWithMandatoryOptionalDate.delete() 20 | await ClassWithMandatoryOptionalDouble.delete() 21 | await ClassWithMandatoryOptionalFloat.delete() 22 | await ClassWithMandatoryOptionalImage.delete() 23 | await ClassWithMandatoryOptionalArrayOfImages.delete() 24 | await ClassWithMandatoryOptionalArrayOfOptionalImages.delete() 25 | await ClassWithMandatoryOptionalArrayOfFloats.delete() 26 | await ClassWithMandatoryOptionalArrayOfOptionalFloats.delete() 27 | await ClassWithMandatoryOptionalArrayOfDoubles.delete() 28 | await ClassWithMandatoryOptionalArrayOfOptionalDoubles.delete() 29 | await ClassWithMandatoryOptionalArrayOfInts.delete() 30 | await ClassWithMandatoryOptionalArrayOfOptionalInts.delete() 31 | await ClassWithMandatoryOptionalArrayOfStrings.delete() 32 | await ClassWithMandatoryOptionalArrayOfOptionalStrings.delete() 33 | await ClassWithMandatoryOptionalArrayOfObjects.delete() 34 | await ClassWithMandatoryOptionalArrayOfOptionalObjects.delete() 35 | await ClassWithMandatoryOptionalObjects.delete() 36 | await MandatoryClass.delete() 37 | completion(nil) 38 | } 39 | } 40 | 41 | func testClassWithMandatoryOptionalString() async { 42 | let mainClass = ClassWithMandatoryOptionalString() 43 | 44 | await mainClass.save() 45 | let retrievedClasses = await ClassWithMandatoryOptionalString.retrieve() 46 | XCTAssertEqual(retrievedClasses.count, 0) 47 | } 48 | 49 | func testClassWithMandatoryOptionalInt() async { 50 | let mainClass = ClassWithMandatoryOptionalInt() 51 | 52 | await mainClass.save() 53 | let retrievedClasses = await ClassWithMandatoryOptionalInt.retrieve() 54 | XCTAssertEqual(retrievedClasses.count, 0) 55 | } 56 | 57 | func testClassWithMandatoryOptionalDate() async { 58 | let mainClass = ClassWithMandatoryOptionalDate() 59 | 60 | await mainClass.save() 61 | let retrievedClasses = await ClassWithMandatoryOptionalDate.retrieve() 62 | XCTAssertEqual(retrievedClasses.count, 0) 63 | } 64 | 65 | func testClassWithMandatoryOptionalDouble() async { 66 | let mainClass = ClassWithMandatoryOptionalDouble() 67 | 68 | await mainClass.save() 69 | let retrievedClasses = await ClassWithMandatoryOptionalDouble.retrieve() 70 | XCTAssertEqual(retrievedClasses.count, 0) 71 | } 72 | 73 | func testClassWithMandatoryOptionalFloat() async { 74 | let mainClass = ClassWithMandatoryOptionalFloat() 75 | 76 | await mainClass.save() 77 | let retrievedClasses = await ClassWithMandatoryOptionalFloat.retrieve() 78 | XCTAssertEqual(retrievedClasses.count, 0) 79 | } 80 | 81 | func testClassWithMandatoryOptionalImage() async { 82 | let mainClass = ClassWithMandatoryOptionalImage() 83 | 84 | await mainClass.save() 85 | let retrievedClasses = await ClassWithMandatoryOptionalImage.retrieve() 86 | XCTAssertEqual(retrievedClasses.count, 0) 87 | } 88 | 89 | func testClassWithMandatoryOptionalArrayOfImages() async { 90 | let mainClass = ClassWithMandatoryOptionalArrayOfImages() 91 | 92 | await mainClass.save() 93 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfImages.retrieve() 94 | XCTAssertEqual(retrievedClasses.count, 0) 95 | } 96 | 97 | func testClassWithMandatoryOptionalArrayOfOptionalImages() async { 98 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalImages() 99 | 100 | await mainClass.save() 101 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalImages.retrieve() 102 | XCTAssertEqual(retrievedClasses.count, 0) 103 | } 104 | 105 | func testClassWithMandatoryOptionalArrayOfFloats() async { 106 | let mainClass = ClassWithMandatoryOptionalArrayOfFloats() 107 | 108 | await mainClass.save() 109 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfFloats.retrieve() 110 | XCTAssertEqual(retrievedClasses.count, 0) 111 | } 112 | 113 | func testClassWithMandatoryOptionalArrayOfOptionalFloats() async { 114 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalFloats() 115 | 116 | await mainClass.save() 117 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalFloats.retrieve() 118 | XCTAssertEqual(retrievedClasses.count, 0) 119 | } 120 | 121 | func testClassWithMandatoryOptionalArrayOfDoubles() async { 122 | let mainClass = ClassWithMandatoryOptionalArrayOfDoubles() 123 | 124 | await mainClass.save() 125 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfDoubles.retrieve() 126 | XCTAssertEqual(retrievedClasses.count, 0) 127 | } 128 | 129 | func testClassWithMandatoryOptionalArrayOfOptionalDoubles() async { 130 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalDoubles() 131 | 132 | await mainClass.save() 133 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalDoubles.retrieve() 134 | XCTAssertEqual(retrievedClasses.count, 0) 135 | } 136 | 137 | func testClassWithMandatoryOptionalArrayOfInts() async { 138 | let mainClass = ClassWithMandatoryOptionalArrayOfInts() 139 | 140 | await mainClass.save() 141 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfInts.retrieve() 142 | XCTAssertEqual(retrievedClasses.count, 0) 143 | } 144 | 145 | func testClassWithMandatoryOptionalArrayOfOptionalInts() async { 146 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalInts() 147 | 148 | await mainClass.save() 149 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalInts.retrieve() 150 | XCTAssertEqual(retrievedClasses.count, 0) 151 | } 152 | 153 | func testClassWithMandatoryOptionalArrayOfStrings() async { 154 | let mainClass = ClassWithMandatoryOptionalArrayOfStrings() 155 | 156 | await mainClass.save() 157 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfStrings.retrieve() 158 | XCTAssertEqual(retrievedClasses.count, 0) 159 | } 160 | 161 | func testClassWithMandatoryOptionalArrayOfOptionalStrings() async { 162 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalStrings() 163 | 164 | await mainClass.save() 165 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalStrings.retrieve() 166 | XCTAssertEqual(retrievedClasses.count, 0) 167 | } 168 | 169 | func testClassWithMandatoryOptionalArrayOfObjects() async { 170 | let mainClass = ClassWithMandatoryOptionalArrayOfObjects() 171 | 172 | await mainClass.save() 173 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfObjects.retrieve() 174 | XCTAssertEqual(retrievedClasses.count, 0) 175 | } 176 | 177 | func testClassWithMandatoryOptionalArrayOfOptionalObjects() async { 178 | let mainClass = ClassWithMandatoryOptionalArrayOfOptionalObjects() 179 | 180 | await mainClass.save() 181 | let retrievedClasses = await ClassWithMandatoryOptionalArrayOfOptionalObjects.retrieve() 182 | XCTAssertEqual(retrievedClasses.count, 0) 183 | } 184 | 185 | func testClassWithMandatoryOptionalObjects() async { 186 | let mainClass = ClassWithMandatoryOptionalObjects() 187 | 188 | await mainClass.save() 189 | let retrievedClasses = await ClassWithMandatoryOptionalObjects.retrieve() 190 | XCTAssertEqual(retrievedClasses.count, 0) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Mandatory Array Operator/ArrayMandatoryTestWithData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayMandatoryTestWithData.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/19/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SundeedQLiteLibrary 11 | 12 | class ArrayMandatoryTestWithData: XCTestCase { 13 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 14 | Task { 15 | await MandatoryClass.delete() 16 | await ClassContainingAMandatoryClassInOptionalArray.delete() 17 | await ClassContainingAMandatoryOptionalClassInArray.delete() 18 | await ClassContainingAMandatoryOptionalClassInOptionalArray.delete() 19 | await ClassContainingAMandatoryClassInArray.delete() 20 | await ClassContainingAMandatoryClass.delete() 21 | await ClassContainingAMandatoryOptionalClass.delete() 22 | await ClassContainingAMandatoryArrayWithData.delete() 23 | await ClassContainingAMandatoryOptionalArrayWithOptionalData.delete() 24 | await ClassContainingAMandatoryArrayWithOptionalData.delete() 25 | await ClassContainingParameterIndex.delete() 26 | await ClassContainingAMandatoryOptionalArrayWithData.delete() 27 | completion(nil) 28 | } 29 | } 30 | 31 | func testClassContainingAMandatoryClassInOptionalArray() async { 32 | let mandatoryClass = MandatoryClass() 33 | let mainClass = ClassContainingAMandatoryClassInOptionalArray() 34 | mainClass.mandatoryClasses = [mandatoryClass] 35 | await mainClass.save() 36 | let retrievedClasses = await ClassContainingAMandatoryClassInOptionalArray.retrieve() 37 | XCTAssertEqual(retrievedClasses.count, 0) 38 | } 39 | 40 | func testClassContainingAMandatoryOptionalClassInArray() async { 41 | let mandatoryClass = MandatoryClass() 42 | let mainClass = ClassContainingAMandatoryOptionalClassInArray() 43 | mainClass.mandatoryClasses = [mandatoryClass] 44 | 45 | await mainClass.save() 46 | let retrievedClasses = await ClassContainingAMandatoryOptionalClassInArray.retrieve() 47 | XCTAssertEqual(retrievedClasses.count, 0) 48 | } 49 | 50 | func testClassContainingAMandatoryOptionalClassInOptionalArray() async { 51 | let mandatoryClass = MandatoryClass() 52 | let mainClass = ClassContainingAMandatoryOptionalClassInOptionalArray() 53 | mainClass.mandatoryClasses = [mandatoryClass] 54 | 55 | await mainClass.save() 56 | let retrievedClasses = await ClassContainingAMandatoryOptionalClassInOptionalArray.retrieve() 57 | XCTAssertEqual(retrievedClasses.count, 0) 58 | } 59 | 60 | func testClassContainingAMandatoryClassInArray() async { 61 | let mandatoryClass = MandatoryClass() 62 | let mainClass = ClassContainingAMandatoryClassInArray() 63 | mainClass.mandatoryClasses = [mandatoryClass] 64 | 65 | await mainClass.save() 66 | let retrievedClasses = await ClassContainingAMandatoryClassInArray.retrieve() 67 | XCTAssertEqual(retrievedClasses.count, 0) 68 | } 69 | 70 | func testClassContainingAMandatoryClass() async { 71 | let mandatoryClass = MandatoryClass() 72 | let mainClass = ClassContainingAMandatoryClass() 73 | mainClass.mandatoryClasses = mandatoryClass 74 | 75 | await mainClass.save() 76 | let retrievedClasses = await ClassContainingAMandatoryClass.retrieve() 77 | XCTAssertEqual(retrievedClasses.count, 0) 78 | } 79 | 80 | func testClassContainingAMandatoryOptionalClass() async { 81 | let mandatoryClass = MandatoryClass() 82 | let mainClass = ClassContainingAMandatoryOptionalClass() 83 | mainClass.mandatoryClasses = mandatoryClass 84 | 85 | await mainClass.save() 86 | let retrievedClasses = await ClassContainingAMandatoryOptionalClass.retrieve() 87 | XCTAssertEqual(retrievedClasses.count, 0) 88 | } 89 | 90 | func testClassContainingAMandatoryArrayWithData() async { 91 | let mandatoryClass = MandatoryClass() 92 | mandatoryClass.firstName = "Test" 93 | let mainClass = ClassContainingAMandatoryArrayWithData() 94 | mainClass.mandatoryClasses = [mandatoryClass] 95 | 96 | await mainClass.save() 97 | let retrievedClasses = await ClassContainingAMandatoryArrayWithData 98 | .retrieve() 99 | XCTAssertEqual(retrievedClasses.count, 1) 100 | } 101 | 102 | func testClassContainingAMandatoryOptionalArrayWithData() async { 103 | let mandatoryClass = MandatoryClass() 104 | mandatoryClass.firstName = "Test" 105 | let mainClass = ClassContainingAMandatoryOptionalArrayWithData() 106 | mainClass.mandatoryClasses = [mandatoryClass] 107 | 108 | await mainClass.save() 109 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithData.retrieve() 110 | XCTAssertEqual(retrievedClasses.count, 1) 111 | } 112 | 113 | func testClassContainingAMandatoryOptionalArrayWithOptionalData() async { 114 | let mandatoryClass = MandatoryClass() 115 | mandatoryClass.firstName = "Test" 116 | let mainClass = ClassContainingAMandatoryOptionalArrayWithOptionalData() 117 | mainClass.mandatoryClasses = [mandatoryClass] 118 | 119 | await mainClass.save() 120 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithOptionalData.retrieve() 121 | XCTAssertEqual(retrievedClasses.count, 1) 122 | } 123 | 124 | func testClassContainingAMandatoryArrayWithOptionalData() async { 125 | let mandatoryClass = MandatoryClass() 126 | mandatoryClass.firstName = "Test" 127 | let mainClass = ClassContainingAMandatoryArrayWithOptionalData() 128 | mainClass.mandatoryClasses = [mandatoryClass] 129 | 130 | await mainClass.save() 131 | let retrievedClasses = await ClassContainingAMandatoryArrayWithOptionalData 132 | .retrieve() 133 | XCTAssertEqual(retrievedClasses.count, 1) 134 | } 135 | 136 | func testClassContainingAMandatoryArrayWithDataWithReference() async { 137 | let mandatoryClass = MandatoryClass() 138 | mandatoryClass.firstName = "Test" 139 | let mainClass = ClassContainingAMandatoryArrayWithData() 140 | mainClass.mandatoryClasses = [mandatoryClass] 141 | 142 | await mainClass.save() 143 | let retrievedClasses = await ClassContainingAMandatoryArrayWithData 144 | .retrieve() 145 | XCTAssertEqual(retrievedClasses.count, 1) 146 | } 147 | 148 | func testClassContainingAMandatoryOptionalArrayWithDataWithReference() async { 149 | let mandatoryClass = MandatoryClass() 150 | mandatoryClass.firstName = "Test" 151 | let mainClass = ClassContainingAMandatoryOptionalArrayWithData() 152 | mainClass.mandatoryClasses = [mandatoryClass] 153 | 154 | await mainClass.save() 155 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithData.retrieve() 156 | XCTAssertEqual(retrievedClasses.count, 1) 157 | } 158 | 159 | func testClassContainingAMandatoryOptionalArrayWithOptionalDataWithReference() async { 160 | let mandatoryClass = MandatoryClass() 161 | mandatoryClass.firstName = "Test" 162 | let mainClass = ClassContainingAMandatoryOptionalArrayWithOptionalData() 163 | mainClass.mandatoryClasses = [mandatoryClass] 164 | 165 | await mainClass.save() 166 | let retrievedClasses = await ClassContainingAMandatoryOptionalArrayWithOptionalData.retrieve() 167 | XCTAssertEqual(retrievedClasses.count, 1) 168 | } 169 | 170 | func testClassContainingAMandatoryArrayWithOptionalDataWithReference() async { 171 | let mandatoryClass = MandatoryClass() 172 | mandatoryClass.firstName = "Test" 173 | let mainClass = ClassContainingAMandatoryArrayWithOptionalData() 174 | mainClass.mandatoryClasses = [mandatoryClass] 175 | 176 | await mainClass.save() 177 | let retrievedClasses = await ClassContainingAMandatoryArrayWithOptionalData 178 | .retrieve() 179 | XCTAssertEqual(retrievedClasses.count, 1) 180 | } 181 | 182 | func testClassContainingParameterIndex() async { 183 | let mainClass = ClassContainingParameterIndex() 184 | 185 | await mainClass.save() 186 | let retrievedClasses = await ClassContainingParameterIndex.retrieve() 187 | XCTAssertEqual(retrievedClasses.count, 0) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Library/ClassHandler/SundeedQLiter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiterClasses.swift 3 | // SQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 12/9/18. 6 | // Copyright © 2018 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SundeedQLiter: AnyObject, Sendable { 12 | /** A function that describes all the mappings between database and object */ 13 | func sundeedQLiterMapping(map: SundeedQLiteMap) 14 | init() 15 | } 16 | 17 | extension SundeedQLiter { 18 | subscript(key: String) -> AnyObject? { 19 | let mirror = Mirror(reflecting: self) 20 | for child in mirror.children where child.label == key { 21 | return child.value as AnyObject 22 | } 23 | return nil 24 | } 25 | /** retrieves the tableName of the specific object*/ 26 | func getTableName() -> String { 27 | return "\(type(of: self))" 28 | } 29 | /** saves the object locally */ 30 | public func save(withForeignKey foreignKey: String? = nil) async { 31 | await SundeedQLite.instance.save(objects: [self], withForeignKey: foreignKey) 32 | SundeedQLite.notify(for: self, operation: .save) 33 | } 34 | 35 | /** deletes the object locally */ 36 | public func delete(deleteSubObjects: Bool = false) async throws { 37 | _ = try await SundeedQLite.instance.deleteFromDB(object: self, 38 | deleteSubObjects: deleteSubObjects) 39 | SundeedQLite.notify(for: self, operation: .delete) 40 | } 41 | 42 | /** updates the object locally */ 43 | public func update(columns: SundeedColumn...) async throws { 44 | SundeedQLite.notify(for: self, operation: .update) 45 | try await SundeedQLite.instance.update(object: self, 46 | columns: columns) 47 | } 48 | 49 | /** retrieves asynchrously all the occurences of a specific class, or add a filter 50 | - author: 51 | Nour Sandid 52 | - returns: 53 | An array of Objects of the specified class 54 | - Parameters: 55 | - filter: (Optional) add a filter to get a specific result 56 | * e.g: SundeedColumn("id") == "A1B2C3" 57 | */ 58 | public static func retrieve(withFilter filter: SundeedExpression?..., 59 | orderBy order: SundeedColumn? = nil, 60 | ascending asc: Bool? = nil, 61 | limit: Int? = nil, 62 | skip: Int? = nil, 63 | excludeIfIsForeign: Bool = true) async -> [Self] { 64 | let objects = await SundeedQLite.instance.retrieve(forClass: self, 65 | withFilter: filter, 66 | orderBy: order, 67 | ascending: asc, 68 | limit: limit, 69 | skip: skip, 70 | excludeIfIsForeign: excludeIfIsForeign) 71 | SundeedQLite.notify(for: objects, operation: .retrieve) 72 | return objects 73 | } 74 | 75 | /** deletes all the objects of this type locally */ 76 | public static func delete(withFilter filters: SundeedExpression...) async { 77 | await SundeedQLite.instance.deleteAllFromDB(forClass: self, 78 | withFilters: filters) 79 | } 80 | 81 | /** updates specific columns of all objects of this class, or objects with a specific criteria */ 82 | public static func update(changes: SundeedUpdateSetStatement..., 83 | withFilter filter: SundeedExpression? = nil) async throws { 84 | try await SundeedQLite.instance.update(forClass: self, 85 | changes: changes, 86 | withFilter: filter) 87 | } 88 | 89 | func toObjectWrapper() -> ObjectWrapper { 90 | let map = SundeedQLiteMap(fetchingColumns: true) 91 | sundeedQLiterMapping(map: map) 92 | var columns = map.columns 93 | for (columnName, value) in map.columns { 94 | if let sundeedObject = value as? SundeedQLiter { 95 | let map = SundeedQLiteMap(fetchingColumns: true) 96 | sundeedObject.sundeedQLiterMapping(map: map) 97 | let nestedColumns: SundeedObject = map.columns.mapValues({ 98 | ($0 as? SundeedQLiter)?.toObjectWrapper() ?? $0 99 | }) 100 | let wrapper = ObjectWrapper(tableName: sundeedObject.getTableName(), 101 | className: "\(sundeedObject)", 102 | objects: nestedColumns, 103 | types: map.types, 104 | isOrdered: map.isOrdered, 105 | orderBy: map.orderBy, 106 | asc: map.asc, 107 | hasPrimaryKey: map.hasPrimaryKey) 108 | columns[columnName] = wrapper 109 | } else if let sundeedObjects = value as? [SundeedQLiter?] { 110 | let dictionnaries = sundeedObjects.compactMap({$0?.toObjectWrapper()}) 111 | columns[columnName] = dictionnaries as AnyObject 112 | } 113 | } 114 | return ObjectWrapper(tableName: getTableName(), 115 | className: "\(self)", 116 | objects: columns, 117 | types: map.types, 118 | isOrdered: map.isOrdered, 119 | orderBy: map.orderBy, 120 | asc: map.asc, 121 | hasPrimaryKey: map.hasPrimaryKey) 122 | } 123 | } 124 | 125 | extension Array where Element: SundeedQLiter { 126 | /** saves the object locally */ 127 | public func save() async { 128 | await SundeedQLite.instance.save(objects: self) 129 | } 130 | } 131 | 132 | // Listener 133 | extension SundeedQLiter { 134 | public static func onAllEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 135 | return SundeedQLite.addListener(object: Self.self, 136 | function: function, 137 | operation: .any) 138 | } 139 | public static func onSaveEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 140 | return SundeedQLite.addListener(object: Self.self, 141 | function: function, 142 | operation: .save) 143 | } 144 | public static func onUpdateEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 145 | return SundeedQLite.addListener(object: Self.self, 146 | function: function, 147 | operation: .update) 148 | } 149 | public static func onDeleteEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 150 | return SundeedQLite.addListener(object: Self.self, 151 | function: function, 152 | operation: .delete) 153 | } 154 | public static func onRetrieveEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 155 | return SundeedQLite.addListener(object: Self.self, 156 | function: function, 157 | operation: .retrieve) 158 | } 159 | public func onAllEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 160 | return SundeedQLite.addSpecificListener(object: self, 161 | function: function, 162 | operation: .any) 163 | } 164 | public func onSaveEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 165 | return SundeedQLite.addSpecificListener(object: self, 166 | function: function, 167 | operation: .save) 168 | } 169 | public func onUpdateEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 170 | return SundeedQLite.addSpecificListener(object: self, 171 | function: function, 172 | operation: .update) 173 | } 174 | public func onDeleteEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 175 | return SundeedQLite.addSpecificListener(object: self, 176 | function: function, 177 | operation: .delete) 178 | } 179 | public func onRetrieveEvents(_ function: @escaping (_ object: Self) -> Void) -> Listener { 180 | return SundeedQLite.addSpecificListener(object: self, 181 | function: function, 182 | operation: .retrieve) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Operations/OperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 5/16/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class OperationTests: XCTestCase { 13 | var employer: EmployerForTesting? = EmployerForTesting() 14 | 15 | override func setUp() { 16 | employer?.fillData() 17 | } 18 | 19 | override func tearDown() async throws { 20 | await EmployerForTesting.delete() 21 | await EmployeeForTesting.delete() 22 | await SeniorEmployeeForTesting.delete() 23 | await JuniorEmployeeForTesting.delete() 24 | } 25 | 26 | func testDeleting() async { 27 | 28 | await employer?.save() 29 | do { 30 | try await self.employer?.delete() 31 | let allEmployers = await EmployerForTesting.retrieve() 32 | XCTAssert(allEmployers.isEmpty) 33 | let allEmployees = await EmployeeForTesting.retrieve(excludeIfIsForeign: false) 34 | XCTAssertEqual(allEmployees.count, 6) 35 | } catch { 36 | XCTFail("Couldn't delete class") 37 | } 38 | } 39 | 40 | func testDeletingWithoutDeletingSubObjects() async { 41 | await employer?.save() 42 | do { 43 | try await self.employer?.delete(deleteSubObjects: false) 44 | let allEmployers = await EmployerForTesting.retrieve() 45 | XCTAssert(allEmployers.isEmpty) 46 | let allEmployees = await EmployeeForTesting.retrieve(excludeIfIsForeign: false) 47 | XCTAssertEqual(allEmployees.count, 6) 48 | } catch { 49 | XCTFail("Couldn't delete class") 50 | } 51 | } 52 | 53 | func testDeletingWithDeletingSubObjects() async { 54 | await employer?.save() 55 | do { 56 | try await self.employer?.delete(deleteSubObjects: true) 57 | let allEmployers = await EmployerForTesting.retrieve() 58 | XCTAssertTrue(allEmployers.isEmpty) 59 | let allEmployees = await EmployeeForTesting.retrieve() 60 | XCTAssertEqual(allEmployees.count, 0) 61 | let allSeniorEmployees = await SeniorEmployeeForTesting.retrieve() 62 | XCTAssertEqual(allSeniorEmployees.count, 0) 63 | let allJuniorEmployees = await JuniorEmployeeForTesting.retrieve() 64 | XCTAssertEqual(allJuniorEmployees.count, 0) 65 | } catch { 66 | XCTFail("Couldn't delete class") 67 | 68 | } 69 | } 70 | 71 | func testDeletingAll() async { 72 | await employer?.save() 73 | await EmployerForTesting.delete() 74 | let allEmployers = await EmployerForTesting.retrieve() 75 | XCTAssert(allEmployers.isEmpty) 76 | let allEmployees = await EmployeeForTesting.retrieve(excludeIfIsForeign: false) 77 | XCTAssertEqual(allEmployees.count, 6) 78 | } 79 | 80 | func testRetrieve() async { 81 | 82 | await employer?.save() 83 | let allEmployers = await EmployerForTesting.retrieve() 84 | guard let employer = allEmployers.first else { 85 | XCTFail("Couldn't Retrieve From Database") 86 | return 87 | } 88 | employer.check() 89 | _ = try? await self.employer?.delete(deleteSubObjects: true) 90 | } 91 | 92 | func testRetrieveForeignObjectWithExcludingIfIsForeignDisabled() async { 93 | 94 | let employees = await EmployeeForTesting.retrieve(excludeIfIsForeign: false) 95 | XCTAssertEqual(employees.count, 0) 96 | await [employer!].save() 97 | let allEmployees = await EmployeeForTesting.retrieve(excludeIfIsForeign: false) 98 | XCTAssertEqual(allEmployees.count, 6) 99 | _ = try? await self.employer?.delete(deleteSubObjects: true) 100 | } 101 | 102 | func testRetrieveForeignObjectWithExcludingIfIsForeignEnabled() async { 103 | 104 | await [employer!].save() 105 | let allEmployees = await EmployeeForTesting.retrieve(excludeIfIsForeign: true) 106 | XCTAssertEqual(allEmployees.count, 0) 107 | _ = try? await self.employer?.delete(deleteSubObjects: true) 108 | } 109 | 110 | func testRetrieveIndependentObjectWithExcludingIfIsForeignDisabled() async { 111 | 112 | await [employer!].save() 113 | let allEmployers = await EmployerForTesting.retrieve(excludeIfIsForeign: false) 114 | XCTAssertEqual(allEmployers.count, 1) 115 | _ = try? await self.employer?.delete(deleteSubObjects: true) 116 | } 117 | 118 | func testRetrieveIndependentObjectWithExcludingIfIsForeignEnabled() async { 119 | 120 | await [self.employer!].save() 121 | let allEmployers = await EmployerForTesting.retrieve(excludeIfIsForeign: true) 122 | XCTAssertEqual(allEmployers.count, 1) 123 | _ = try? await self.employer?.delete(deleteSubObjects: true) 124 | } 125 | 126 | func testNoPrimaryForClassWithSubclass() async { 127 | let classWithNoPrimaryWithSubClass = ClassWithNoPrimaryWithSubClass() 128 | classWithNoPrimaryWithSubClass.fillData() 129 | 130 | await classWithNoPrimaryWithSubClass.save() 131 | let results = await ClassWithNoPrimaryWithSubClass.retrieve() 132 | XCTAssert(results.isEmpty) 133 | let _ = try? await classWithNoPrimaryWithSubClass.delete(deleteSubObjects: true) 134 | 135 | } 136 | 137 | func testNoPrimaryForClassWithSubclassArray() async { 138 | let classWithNoPrimaryWithSubClassArray = ClassWithNoPrimaryWithSubClassArray() 139 | classWithNoPrimaryWithSubClassArray.fillData() 140 | 141 | await classWithNoPrimaryWithSubClassArray.save() 142 | let results = await ClassWithNoPrimaryWithSubClassArray.retrieve() 143 | XCTAssert(results.isEmpty) 144 | let _ = try? await classWithNoPrimaryWithSubClassArray.delete(deleteSubObjects: true) 145 | } 146 | 147 | func testNoPrimaryForClassWithImage() async { 148 | let classWithNoPrimaryWithImage = ClassWithNoPrimaryWithImage() 149 | classWithNoPrimaryWithImage.fillData() 150 | 151 | await classWithNoPrimaryWithImage.save() 152 | let results = await ClassWithNoPrimaryWithImage.retrieve() 153 | XCTAssert(results.isEmpty) 154 | let _ = try? await classWithNoPrimaryWithImage.delete(deleteSubObjects: true) 155 | } 156 | 157 | func testNoPrimaryForClassWithImageArray() async { 158 | 159 | await ClassWithNoPrimaryWithImageArray.delete() 160 | let classWithNoPrimaryWithImageArray = ClassWithNoPrimaryWithImageArray() 161 | classWithNoPrimaryWithImageArray.fillData() 162 | await classWithNoPrimaryWithImageArray.save() 163 | let results = await ClassWithNoPrimaryWithImageArray.retrieve() 164 | XCTAssertEqual(results.count, 1) 165 | XCTAssertNil(results.first?.images) 166 | let _ = try? await classWithNoPrimaryWithImageArray.delete(deleteSubObjects: true) 167 | 168 | } 169 | 170 | func testNoPrimaryForClassWithPrimitiveArray() async { 171 | let classWithNoPrimaryWithPrimitiveArray = ClassWithNoPrimaryWithPrimitiveArray() 172 | classWithNoPrimaryWithPrimitiveArray.fillData() 173 | 174 | await classWithNoPrimaryWithPrimitiveArray.save() 175 | let results = await ClassWithNoPrimaryWithPrimitiveArray.retrieve() 176 | XCTAssert(results.isEmpty) 177 | let _ = try? await classWithNoPrimaryWithPrimitiveArray.delete(deleteSubObjects: true) 178 | } 179 | 180 | func testNoPrimaryForClassWithDate() async { 181 | let classWithNoPrimaryWithDate = ClassWithNoPrimaryWithDate() 182 | classWithNoPrimaryWithDate.fillData() 183 | 184 | await classWithNoPrimaryWithDate.save() 185 | let results = await ClassWithNoPrimaryWithDate.retrieve() 186 | XCTAssert(results.isEmpty) 187 | let _ = try? await classWithNoPrimaryWithDate.delete(deleteSubObjects: true) 188 | } 189 | 190 | func testArraySaving() async { 191 | let employer2 = EmployerForTesting() 192 | employer2.fillData() 193 | employer2.string = "string1" 194 | employer2.integer = 2 195 | 196 | await [employer2, employer!].save() 197 | let allEmployers = await EmployerForTesting.retrieve() 198 | guard let employer1 = allEmployers.first else { 199 | XCTFail("Couldn't Retrieve From Database") 200 | return 201 | } 202 | allEmployers[1].check() 203 | XCTAssertEqual(employer1.integer, 2) 204 | XCTAssertEqual(employer1.string, "string1") 205 | let _ = try? await self.employer?.delete(deleteSubObjects: true) 206 | let _ = try? await employer2.delete(deleteSubObjects: true) 207 | 208 | } 209 | 210 | func testUpdateWithNoChanges() async { 211 | do { 212 | try await EmployerForTesting.update() 213 | } catch { 214 | guard let sundeedError = error as? SundeedQLiteError else { 215 | XCTFail("Wrong Error") 216 | return 217 | } 218 | XCTAssertEqual(sundeedError.description, SundeedQLiteError.noChangesMade(tableName: "EmployerForTesting").description) 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Library/Main/Processor/Processors/RetrieveProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedRetrieveProcessor.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/11/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SQLite3 11 | 12 | class RetrieveProcessor: Processor { 13 | func retrieve(objectWrapper: ObjectWrapper, 14 | orderBy order: SundeedColumn? = nil, 15 | ascending: Bool? = nil, 16 | withFilter filters: [SundeedExpression?] = [], 17 | limit: Int? = nil, 18 | skip: Int? = nil, 19 | excludeIfIsForeign: Bool = false, 20 | subObjectHandler: (_ objectType: String) -> ObjectWrapper?) -> [SundeedObject] { 21 | let database: OpaquePointer? = SundeedQLiteConnection.pool.connection() 22 | let columns = getDatabaseColumns(forTable: objectWrapper.tableName) 23 | SundeedLogger.info("Retrieving \(objectWrapper.tableName)") 24 | if !columns.isEmpty { 25 | var statement: OpaquePointer? 26 | let query: String? = StatementBuilder() 27 | .selectStatement(tableName: objectWrapper.tableName) 28 | .isOrdered(order != nil || objectWrapper.isOrdered) 29 | .orderBy(columnName: order?.value ?? objectWrapper.orderBy) 30 | .isAscending(ascending ?? objectWrapper.asc) 31 | .isCaseInsensitive(true) 32 | .withFilters(filters) 33 | .limit(limit) 34 | .skip(skip) 35 | .excludeIfIsForeign(excludeIfIsForeign) 36 | .build() 37 | 38 | sqlite3_prepare_v2(database, query, -1, &statement, nil) 39 | let array: [[String: Any]] = fetchStatementResult(statement: statement, 40 | columns: columns, 41 | objectWrapper: objectWrapper, 42 | subObjectHandler: subObjectHandler) 43 | SundeedLogger.debug("Found for \(objectWrapper.tableName): \(array)") 44 | SundeedQLiteConnection.pool.closeConnection(database) 45 | return array 46 | } else { 47 | SundeedQLiteConnection.pool.closeConnection(database) 48 | } 49 | return [] 50 | } 51 | 52 | func fetchStatementResult(statement: OpaquePointer?, 53 | columns: [(columnName: String, columnType: ParameterType)], 54 | objectWrapper: ObjectWrapper, 55 | subObjectHandler: (_ objectType: String) -> ObjectWrapper?) -> [[String: Any]] { 56 | var array: [[String: Any]] = [] 57 | while sqlite3_step(statement) == SQLITE_ROW { 58 | var dictionary: [String: Any] = [:] 59 | var primaryValue: String? 60 | for (index, column) in columns.enumerated() { 61 | if sqlite3_column_type(statement, Int32(index)) == SQLITE_NULL { 62 | let columnName = column.columnName 63 | dictionary[columnName] = nil 64 | } else if SQLITE_BLOB == sqlite3_column_type(statement, Int32(index)), 65 | let databaseValue = sqlite3_column_blob(statement, Int32(index)) { 66 | let size = Int(sqlite3_column_bytes(statement, Int32(index))) 67 | let value: Data = Data(bytes: databaseValue, count: size) 68 | dictionary[column.columnName] = value 69 | } else if case .integer = column.columnType { 70 | let databaseValue = sqlite3_column_int(statement, Int32(index)) 71 | let value: Int = Int(databaseValue) 72 | let columnName = column.columnName 73 | dictionary[columnName] = value 74 | } else if case .double = column.columnType { 75 | let databaseValue = sqlite3_column_double(statement, Int32(index)) 76 | let value: Double = Double(databaseValue) 77 | let columnName = column.columnName 78 | dictionary[columnName] = value 79 | } else if let databaseValue = sqlite3_column_text(statement, Int32(index)) { 80 | let value: String = normalizeColumnValue(databaseValue) 81 | let columnName = column.columnName 82 | if value != Sundeed.shared.databaseNull { 83 | dictionary[columnName] = value 84 | } 85 | if columnName == Sundeed.shared.primaryKey { 86 | primaryValue = value 87 | } 88 | } 89 | } 90 | fetchForeignObjects(withObject: objectWrapper, 91 | primaryValue: primaryValue, 92 | inDictionary: &dictionary, 93 | subObjectHandler: subObjectHandler) 94 | array.append(dictionary) 95 | } 96 | return array 97 | } 98 | 99 | func fetchForeignObjects(withObject objectWrapper: ObjectWrapper, 100 | primaryValue: String?, 101 | inDictionary dictionary: inout [String: Any], 102 | subObjectHandler: (_ objectType: String) -> ObjectWrapper?) { 103 | guard let primaryValue = primaryValue else { return } 104 | for row in dictionary { 105 | if let value = row.value as? String { 106 | if value.starts(with: Sundeed.shared.foreignPrefix) { 107 | let configurations = value.split(separator: "|") 108 | let embededElementTable = String(describing: configurations[1]) 109 | let embededElementFieldNameLink = String(configurations[2]) 110 | var embededElementPrimaryKey: String? 111 | if configurations.count == 4 { 112 | embededElementPrimaryKey = String(configurations[3]) 113 | } 114 | if let subObject = subObjectHandler(embededElementTable) { 115 | let filter1 = SundeedColumn(Sundeed.shared.foreignKey) == primaryValue 116 | let filter2 = SundeedColumn(Sundeed.shared.fieldNameLink) == embededElementFieldNameLink 117 | var filter3: SundeedExpression? 118 | if let embededElementPrimaryKey = embededElementPrimaryKey { 119 | filter3 = SundeedColumn(Sundeed.shared.primaryKey) == embededElementPrimaryKey 120 | } 121 | dictionary[row.key] = self 122 | .retrieve(objectWrapper: subObject, 123 | withFilter: [filter1, filter2, filter3], 124 | excludeIfIsForeign: false, 125 | subObjectHandler: subObjectHandler) 126 | } 127 | } else if value.starts(with: Sundeed.shared.foreignPrimitivePrefix) { 128 | let configurations = value.split(separator: "|") 129 | let embededElementTable = String(configurations[1]) 130 | let filter = SundeedColumn(Sundeed.shared.foreignKey) == primaryValue 131 | dictionary[row.key] = self.getPrimitiveValues(forTable: embededElementTable, 132 | withFilter: filter) 133 | } 134 | } 135 | } 136 | } 137 | func getPrimitiveValues(forTable table: String, 138 | withFilter filter: SundeedExpression?) -> [Any]? { 139 | let database = SundeedQLiteConnection.pool.connection() 140 | var statement: OpaquePointer? 141 | let selectStatement = StatementBuilder() 142 | .selectStatement(tableName: table) 143 | .withFilters(filter) 144 | .build() 145 | if sqlite3_prepare_v2(database, selectStatement, -1, &statement, nil) == SQLITE_OK { 146 | let columns = getDatabaseColumns(forTable: table) 147 | var array: [Any] = [] 148 | while sqlite3_step(statement) == SQLITE_ROW { 149 | for (index, column) in columns.enumerated() where column.columnName == Sundeed.shared.valueColumnName { 150 | if SQLITE_BLOB == sqlite3_column_type(statement, Int32(index)), 151 | let databaseValue = sqlite3_column_blob(statement, Int32(index)) { 152 | let size = Int(sqlite3_column_bytes(statement, Int32(index))) 153 | let value: Data = Data(bytes: databaseValue, count: size) 154 | array.append(value) 155 | } else if case .double = column.columnType { 156 | let databaseValue = sqlite3_column_double(statement, Int32(index)) 157 | let value: Double = Double(databaseValue) 158 | array.append(value) 159 | } else if case .integer = column.columnType { 160 | let databaseValue = sqlite3_column_int(statement, Int32(index)) 161 | let value: Int = Int(databaseValue) 162 | array.append(value) 163 | } else if let columnValue = sqlite3_column_text(statement, Int32(index)) { 164 | let value: String = String(cString: columnValue) 165 | if value != Sundeed.shared.databaseNull { 166 | array.append(value.replacingOccurrences(of: "\\\"", with: "\"")) 167 | } 168 | } 169 | } 170 | } 171 | SundeedQLiteConnection.pool.closeConnection(database) 172 | return array 173 | } else { 174 | SundeedQLiteConnection.pool.closeConnection(database) 175 | } 176 | return nil 177 | } 178 | func normalizeColumnValue(_ columnValue: UnsafePointer) -> String { 179 | String(cString: columnValue).replacingOccurrences(of: "\\\"", with: "\"") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Library/Main/Processor/Processors/UpdateProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedUpdateProcessor.swift 3 | // SundeedQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 5/11/20. 6 | // Copyright © 2020 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UpdateProcessor: Processor { 12 | func update(objectWrapper: ObjectWrapper, 13 | columns: [SundeedColumn], 14 | withFilters filters: [SundeedExpression?]) async throws { 15 | SundeedLogger.info("Updating \(objectWrapper.tableName)") 16 | var depth: Int = 1 17 | try Processors() 18 | .createTableProcessor 19 | .createTableIfNeeded(for: objectWrapper) 20 | guard objectWrapper.hasPrimaryKey else { 21 | throw SundeedQLiteError 22 | .primaryKeyError(tableName: objectWrapper.tableName) 23 | } 24 | let updateStatement = StatementBuilder() 25 | .updateStatement(tableName: objectWrapper.tableName) 26 | for column in columns { 27 | if let objects = objectWrapper.objects, 28 | objects.contains(where: { (arg0) -> Bool in 29 | let (key, _) = arg0 30 | return key == column.value 31 | }) { 32 | let attribute = objects[column.value] 33 | if let attribute = attribute as? ObjectWrapper, 34 | let className = attribute.className { 35 | if let primaryValue = objects[Sundeed.shared.primaryKey] as? String { 36 | SundeedLogger.debug("Updating foreign object found for \(objectWrapper.tableName) at property \(column): \(attribute.tableName) with Primary/foreign key: \(primaryValue)") 37 | depth += 1 38 | await self.saveForeignObjects(attributes: [attribute], 39 | primaryValue: primaryValue, 40 | column: column, 41 | className: className, 42 | updateStatement: updateStatement) 43 | } else { 44 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 45 | } 46 | } else if let attributes = attribute as? [ObjectWrapper?] { 47 | if let firstAttribute = attributes.first as? ObjectWrapper, 48 | let className = firstAttribute.className { 49 | if let primaryValue = objects[Sundeed.shared.primaryKey] as? String { 50 | SundeedLogger.debug("Updating array of foreign objects found for \(objectWrapper.tableName) at property \(column): \(firstAttribute.tableName) with Primary/foreign key: \(primaryValue)") 51 | await self.saveForeignObjects(attributes: attributes, 52 | primaryValue: primaryValue, 53 | column: column, 54 | className: className, 55 | updateStatement: updateStatement) 56 | } else { 57 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 58 | } 59 | } 60 | } else if let attribute = attribute as? UIImage { 61 | if let primaryValue = objects[Sundeed.shared.primaryKey] as? String { 62 | SundeedLogger.debug("Updating image found for \(objectWrapper.tableName) at property \(column) with Primary/foreign key: \(primaryValue)") 63 | updateStatement 64 | .add(key: column.value, 65 | value: attribute 66 | .dataTypeValue(forObjectID: primaryValue)) 67 | } else { 68 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 69 | } 70 | } else if let attribute = attribute as? Date { 71 | SundeedLogger.debug("Updating date found for \(objectWrapper.tableName) at property \(column)") 72 | updateStatement.add(key: column.value, 73 | value: attribute.timeIntervalSince1970*1000) 74 | } else if let attribute = attribute as? Data { 75 | if objects[Sundeed.shared.primaryKey] as? String != nil { 76 | SundeedLogger.debug("Updating data found for \(objectWrapper.tableName) at property \(column)") 77 | updateStatement.add(key: column.value, 78 | value: attribute) 79 | } else { 80 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 81 | } 82 | } else if let attributes = attribute as? [UIImage?] { 83 | let attributes = attributes.compactMap({$0}) 84 | if !attributes.isEmpty { 85 | if let primaryValue = objects[Sundeed.shared.primaryKey] as? String { 86 | SundeedLogger.debug("Updating array of images found for \(objectWrapper.tableName) at property \(column) with Primary/foreign key: \(primaryValue)") 87 | depth += 1 88 | await self.saveArrayOfImages(attributes: attributes, 89 | primaryValue: primaryValue, 90 | column: column, 91 | updateStatement: updateStatement) 92 | } else { 93 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 94 | } 95 | } 96 | } else if let attributes = attribute as? [Any] { 97 | let attributes = attributes.compactMap({$0}) 98 | if !attributes.isEmpty { 99 | if let primaryValue = objects[Sundeed.shared.primaryKey] as? String { 100 | SundeedLogger.debug("Updating array of primitive datatype found for \(objectWrapper.tableName) at property \(column) with Primary/foreign key: \(primaryValue)") 101 | depth += 1 102 | await Processors() 103 | .saveProcessor 104 | .saveArrayOfPrimitives(tableName: column.value, 105 | objects: attributes, 106 | withForeignKey: primaryValue) 107 | updateStatement 108 | .add(key: column.value, 109 | value: Sundeed.shared 110 | .sundeedPrimitiveForeignValue(tableName: column.value)) 111 | } else { 112 | throw SundeedQLiteError.primaryKeyError(tableName: objectWrapper.tableName) 113 | } 114 | } 115 | } else { 116 | if let attribute = attribute { 117 | let attributeString = "\(attribute as AnyObject)" 118 | updateStatement.add(key: column.value, 119 | value: attributeString) 120 | } else { 121 | updateStatement.add(key: column.value, 122 | value: "") 123 | } 124 | } 125 | } else { 126 | throw SundeedQLiteError 127 | .noColumnWithThisName(tableName: objectWrapper.tableName, 128 | columnName: column.value) 129 | } 130 | } 131 | updateStatement.withFilters(filters) 132 | let statement = updateStatement.build() 133 | SundeedQLiteConnection.pool.execute(query: statement?.query, 134 | parameters: statement?.parameters) 135 | } 136 | 137 | func saveArrayOfImages(attributes: [UIImage], 138 | primaryValue: String, 139 | column: SundeedColumn, 140 | updateStatement: UpdateStatement) async { 141 | let attributes: [String] = attributes.enumerated() 142 | .map({ 143 | let index = $0 144 | let indexString = String(describing: index) 145 | let objectID = "\(primaryValue)\(column.value)\(indexString)" 146 | return $1.dataTypeValue(forObjectID: objectID) 147 | }) 148 | await Processors() 149 | .saveProcessor 150 | .saveArrayOfPrimitives(tableName: column.value, 151 | objects: attributes, 152 | withForeignKey: primaryValue) 153 | updateStatement 154 | .add(key: column.value, 155 | value: Sundeed.shared 156 | .sundeedPrimitiveForeignValue(tableName: column.value)) 157 | } 158 | 159 | 160 | func saveForeignObjects(attributes: [ObjectWrapper?], 161 | primaryValue: String, 162 | column: SundeedColumn, 163 | className: String, 164 | updateStatement: UpdateStatement) async { 165 | await Processors() 166 | .saveProcessor 167 | .save(objects: attributes.compactMap({$0}), 168 | withForeignKey: primaryValue, 169 | withFieldNameLink: column.value) 170 | 171 | updateStatement 172 | .add(key: column.value, 173 | value: Sundeed.shared 174 | .sundeedForeignValue(tableName: className, 175 | fieldNameLink: column.value)) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SundeedQLiteLibraryTests/Main/SundeedQLiteLibraryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteLibraryTests.swift 3 | // SundeedQLiteLibraryTests 4 | // 5 | // Created by Nour Sandid on 10/5/19. 6 | // Copyright © 2019 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SundeedQLiteLibrary 11 | 12 | class SundeedQLiteLibraryTests: XCTestCase { 13 | var employer: EmployerForTesting = EmployerForTesting() 14 | 15 | override func setUp() async throws { 16 | employer.fillData() 17 | await employer.save() 18 | } 19 | override func tearDown(completion: @escaping ((any Error)?) -> Void) { 20 | Task { 21 | try await employer.delete(deleteSubObjects: true) 22 | completion(nil) 23 | } 24 | } 25 | 26 | func testSubscript() { 27 | guard let string = self.employer["string"] as? String else { 28 | XCTFail("Employer is nil") 29 | return 30 | } 31 | XCTAssertEqual(string, "string") 32 | } 33 | 34 | func testWrongSubscript() { 35 | let string = employer["wrong"] as? String 36 | XCTAssertNil(string) 37 | } 38 | 39 | func testSundeedQLiteMap() { 40 | let employee = EmployeeForTesting() 41 | SundeedQLiteMap.references["Test"] = nil 42 | SundeedQLiteMap.addReference(object: employee, 43 | andValue: "ID_ID" as AnyObject, 44 | andClassName: "Test") 45 | if let reference = SundeedQLiteMap.references["Test"], 46 | let employeeRetrieved = reference["ID_ID"] as? EmployeeForTesting { 47 | XCTAssertEqual(employee.id, employeeRetrieved.id) 48 | } else { 49 | XCTFail("Reference nil") 50 | } 51 | } 52 | func testSaveProcessorAcceptData() { 53 | let object: String = "" 54 | XCTAssert(SaveProcessor().acceptDataType(forObject: object as AnyObject)) 55 | let object1: String? = "" 56 | XCTAssert(SaveProcessor().acceptDataType(forObject: object1 as AnyObject)) 57 | let object2: Int = 1 58 | XCTAssert(SaveProcessor().acceptDataType(forObject: object2 as AnyObject)) 59 | let object3: Int? = 1 60 | XCTAssert(SaveProcessor().acceptDataType(forObject: object3 as AnyObject)) 61 | let object4: Double = 1.0 62 | XCTAssert(SaveProcessor().acceptDataType(forObject: object4 as AnyObject)) 63 | let object5: Double? = 1.0 64 | XCTAssert(SaveProcessor().acceptDataType(forObject: object5 as AnyObject)) 65 | let object6: Float = 1.0 66 | XCTAssert(SaveProcessor().acceptDataType(forObject: object6 as AnyObject)) 67 | let object7: Float? = 1.0 68 | XCTAssert(SaveProcessor().acceptDataType(forObject: object7 as AnyObject)) 69 | let object8: Bool = true 70 | XCTAssert(SaveProcessor().acceptDataType(forObject: object8 as AnyObject)) 71 | let object9: Bool? = true 72 | XCTAssert(SaveProcessor().acceptDataType(forObject: object9 as AnyObject)) 73 | let object10: Date = Date() 74 | XCTAssert(SaveProcessor().acceptDataType(forObject: object10 as AnyObject)) 75 | let object11: Date? = Date() 76 | XCTAssert(SaveProcessor().acceptDataType(forObject: object11 as AnyObject)) 77 | let object12: UIImage = UIImage(named: "image")! 78 | XCTAssert(SaveProcessor().acceptDataType(forObject: object12 as AnyObject)) 79 | let object13: UIImage? = UIImage(named: "image") 80 | XCTAssert(SaveProcessor().acceptDataType(forObject: object13 as AnyObject)) 81 | XCTAssertFalse(SaveProcessor().acceptDataType(forObject: nil)) 82 | XCTAssertFalse(SaveProcessor().acceptDataType(forObject: Type.ceo as AnyObject)) 83 | } 84 | 85 | func testRetrieveWithWrongTableName() { 86 | let objectWrapper = ObjectWrapper(tableName: "HHH", 87 | className: "HHH", 88 | objects: [:], 89 | types: [:]) 90 | let result = RetrieveProcessor().retrieve(objectWrapper: objectWrapper) { _ -> ObjectWrapper? in 91 | return nil 92 | } 93 | XCTAssertEqual(result.count, 0) 94 | } 95 | 96 | func testGetPrimitiveValuesWithWrongTableName() { 97 | let result = RetrieveProcessor().getPrimitiveValues(forTable: "WrongTableName", 98 | withFilter: nil) 99 | XCTAssertNil(result) 100 | } 101 | 102 | func testUpdateWithNoFilter() { 103 | let query = UpdateStatement(with: "EmployerForTesting") 104 | .add(key: "string", value: "value1") 105 | .build() 106 | XCTAssertEqual(query?.query, "UPDATE EmployerForTesting SET string = ? WHERE 1") 107 | XCTAssertEqual(query?.parameters.count, 1) 108 | switch query?.parameters.first { 109 | case .text(let text): 110 | XCTAssertEqual(text, "value1") 111 | case .integer: 112 | XCTFail("UPDATE IS NOT INTEGER") 113 | case .double: 114 | XCTFail("UPDATE IS NOT DOUBLE") 115 | case .blob: 116 | XCTFail("UPDATE IS NOT BLOB") 117 | case .none: 118 | XCTFail("PARAMETERS SHOULDN'T BE NIL") 119 | } 120 | } 121 | 122 | func testUpdateIntegerWithNoFilter() { 123 | let query = UpdateStatement(with: "EmployerForTesting") 124 | .add(key: "integer", value: 1) 125 | .build() 126 | XCTAssertEqual(query?.query, "UPDATE EmployerForTesting SET integer = ? WHERE 1") 127 | XCTAssertEqual(query?.parameters.count, 1) 128 | switch query?.parameters.first { 129 | case .text: 130 | XCTFail("UPDATE IS NOT TEXT") 131 | case .integer(let integer): 132 | XCTAssertEqual(integer, 1) 133 | case .double: 134 | XCTFail("UPDATE IS NOT DOUBLE") 135 | case .blob: 136 | XCTFail("UPDATE IS NOT BLOB") 137 | case .none: 138 | XCTFail("PARAMETERS SHOULDN'T BE NIL") 139 | } 140 | } 141 | 142 | func testUpdateDoubleWithNoFilter() { 143 | 144 | let query = UpdateStatement(with: "EmployerForTesting") 145 | .add(key: "double", value: 1) 146 | .build() 147 | XCTAssertEqual(query?.query, "UPDATE EmployerForTesting SET double = ? WHERE 1") 148 | XCTAssertEqual(query?.parameters.count, 1) 149 | switch query?.parameters.first { 150 | case .text: 151 | XCTFail("UPDATE IS NOT TEXT") 152 | case .integer: 153 | XCTFail("UPDATE IS NOT INTEGER") 154 | case .double(let double): 155 | XCTAssertEqual(double, 1) 156 | case .blob: 157 | XCTFail("UPDATE IS NOT BLOB") 158 | case .none: 159 | XCTFail("PARAMETERS SHOULDN'T BE NIL") 160 | } 161 | } 162 | 163 | func testUpdateDataWithNoFilter() { 164 | let query = UpdateStatement(with: "EmployerForTesting") 165 | .add(key: "data", value: "value1".data(using: .utf8)) 166 | .build() 167 | XCTAssertEqual(query?.query, "UPDATE EmployerForTesting SET data = ? WHERE 1") 168 | XCTAssertEqual(query?.parameters.count, 1) 169 | switch query?.parameters.first { 170 | case .text: 171 | XCTFail("UPDATE IS NOT TEXT") 172 | case .integer: 173 | XCTFail("UPDATE IS NOT INTEGER") 174 | case .double: 175 | XCTFail("UPDATE IS NOT DOUBLE") 176 | case .blob(let data): 177 | if let data { 178 | XCTAssertEqual(String(data: data, encoding: .utf8), "value1") 179 | } else { 180 | XCTFail("Data is nil") 181 | } 182 | case .none: 183 | XCTFail("PARAMETERS SHOULDN'T BE NIL") 184 | } 185 | } 186 | 187 | func testDeleteWithNoFilter() { 188 | let query = DeleteStatement(with: "table") 189 | .build() 190 | XCTAssertEqual(query, "DELETE FROM table WHERE 1") 191 | } 192 | 193 | func testQuotationsChange() { 194 | let quotationForSingleQuote = Statement().getQuotation(forValue: "trying ''") 195 | XCTAssertEqual(quotationForSingleQuote, "\"") 196 | let quotationForDoubleQuote = Statement().getQuotation(forValue: "trying \"\"") 197 | XCTAssertEqual(quotationForDoubleQuote, "\'") 198 | } 199 | 200 | func testClassToObjectWrapperWithWrongClass() { 201 | let wrapper = SundeedQLite.instance.classToObjectWrapper("WrongClass") 202 | XCTAssertNil(wrapper) 203 | } 204 | 205 | func testDeleteWithNoPrimary()async { 206 | do { 207 | _ = try await SundeedQLite.instance.deleteFromDB(object: ClassWithNoPrimary(), deleteSubObjects: false) 208 | XCTFail("Shouldn't continue") 209 | } catch { 210 | guard let sundeedError = error as? SundeedQLiteError else { 211 | XCTFail("Wrong Error") 212 | return 213 | } 214 | XCTAssertEqual(sundeedError.description, SundeedQLiteError.primaryKeyError(tableName: "ClassWithNoPrimary").description) 215 | } 216 | } 217 | 218 | func testCreateTableWithNilObjectWrapper() async { 219 | let objectWrapper = ObjectWrapper(tableName: "Table", 220 | className: "Class", 221 | objects: nil, 222 | types: nil) 223 | do { 224 | try CreateTableProcessor().createTableIfNeeded(for: objectWrapper) 225 | XCTFail("Weirdly it continued without throwing an error") 226 | } catch { 227 | guard let sundeedError = error as? SundeedQLiteError else { 228 | XCTFail("Wrong Error") 229 | return 230 | } 231 | XCTAssertEqual(sundeedError.description, SundeedQLiteError.noObjectPassed.description) 232 | } 233 | } 234 | 235 | func testDeleteDatabase() async { 236 | await employer.save() 237 | SundeedQLite.deleteDatabase() 238 | let retrieved = await EmployerForTesting.retrieve() 239 | XCTAssert(retrieved.isEmpty) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Library/Main/SundeedQLiteConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SundeedQLiteConnection.swift 3 | // SQLiteLibrary 4 | // 5 | // Created by Nour Sandid on 12/9/18. 6 | // Copyright © 2018 LUMBERCODE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SQLite3 11 | 12 | enum ParameterType { 13 | case text(String?) 14 | case blob(Data?) 15 | case integer(Int?) 16 | case double(Double?) 17 | 18 | var rawValue: String { 19 | return switch self { 20 | case .text: "TEXT" 21 | case .blob: "BLOB" 22 | case .integer: "INTEGER" 23 | case .double: "DOUBLE" 24 | } 25 | } 26 | 27 | init(typeString: String, value: Any? = nil) { 28 | switch typeString { 29 | case "BLOB": 30 | self = .blob((value as? Data) ?? Data()) 31 | case "INTEGER": 32 | self = .integer((value as? Int) ?? 0) 33 | case "DOUBLE": 34 | self = .double((value as? Double) ?? 0) 35 | default: 36 | self = .text((value as? String) ?? "") 37 | } 38 | } 39 | 40 | func withValue(_ value: Any?) -> Self { 41 | switch self { 42 | case .blob: 43 | return .blob((value as? Data)) 44 | case .integer: 45 | if let value = value as? NSNumber { 46 | return .integer(Int(truncating: value)) 47 | } else { 48 | return .integer(nil) 49 | } 50 | case .double: 51 | if let value = value as? NSNumber { 52 | return .double(Double(truncating: value)) 53 | } else { 54 | return .double(nil) 55 | } 56 | default: 57 | if let value { 58 | return .text(String(describing: value)) 59 | } else { 60 | return .text(nil) 61 | } 62 | } 63 | } 64 | } 65 | class SundeedQLiteConnection { 66 | static var pool: SundeedQLiteConnection = SundeedQLiteConnection() 67 | var sqlStatements: [(query: String, parameters: [ParameterType]?)] = [] 68 | var canExecute: Bool = true 69 | let fileManager = FileManager.default 70 | let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) 71 | private let queue = DispatchQueue(label: "thread-safe-statements", attributes: .concurrent) 72 | private let connectionQueue = DispatchQueue(label: "thread-safe-connection", attributes: .concurrent) 73 | private let backgroundQueue = DispatchQueue(label: "thread-safe-background-statements", attributes: .concurrent) 74 | private var connections: [OpaquePointer] = [] 75 | 76 | init() { 77 | try? createDatabaseIfNeeded() 78 | } 79 | 80 | lazy var fullDestPath: URL? = { 81 | do { 82 | return try FileManager 83 | .default.url(for: .documentDirectory, 84 | in: .userDomainMask, 85 | appropriateFor: nil, 86 | create: true) 87 | .appendingPathComponent(Sundeed.shared.databaseFileName) 88 | } catch { 89 | SundeedLogger.error(error) 90 | return nil 91 | } 92 | }() 93 | 94 | func connection(write: Bool = false) -> OpaquePointer? { 95 | connectionQueue.sync(flags: .barrier) { 96 | guard let path = fullDestPath?.path else { 97 | SundeedLogger.error("DB path missing") 98 | return nil 99 | } 100 | 101 | var connection: OpaquePointer? 102 | let flags = write ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX): (SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX) 103 | let rc = sqlite3_open_v2(path, &connection, flags, nil) 104 | if rc != SQLITE_OK { 105 | if let connection, let cmsg = sqlite3_errmsg(connection) { 106 | SundeedLogger.debug(String(cString: cmsg)) 107 | } else { 108 | SundeedLogger.debug("sqlite3_open_v2 returned \(rc)") 109 | } 110 | if connection != nil { 111 | sqlite3_close_v2(connection) 112 | } 113 | return nil 114 | } 115 | 116 | let timeout: Int32 = 500 117 | if sqlite3_busy_timeout(connection, timeout) != SQLITE_OK { 118 | if let cmsg = sqlite3_errmsg(connection) { 119 | SundeedLogger.debug("Failed set busy_timeout: \(String(cString: cmsg))") 120 | } 121 | } 122 | 123 | return connection 124 | } 125 | } 126 | 127 | func closeConnection(_ connection: OpaquePointer?) { 128 | connectionQueue.sync(flags: .barrier) { 129 | guard let db = connection else { return } 130 | 131 | while let stmt = sqlite3_next_stmt(db, nil) { 132 | sqlite3_finalize(stmt) 133 | } 134 | 135 | #if SQLITE_ENABLE_COLUMN_METADATA 136 | let rc = sqlite3_close_v2(db) 137 | #else 138 | let rc = sqlite3_close(db) 139 | #endif 140 | 141 | if rc != SQLITE_OK && rc != SQLITE_DONE { 142 | if let cmsg = sqlite3_errmsg(db) { 143 | SundeedLogger.debug("sqlite3_close returned \(rc): \(String(cString: cmsg))") 144 | } else { 145 | SundeedLogger.debug("sqlite3_close returned \(rc)") 146 | } 147 | } 148 | } 149 | } 150 | func execute(query: String?, parameters: [ParameterType]? = nil, force: Bool = false) { 151 | queue.sync(flags: .barrier) { 152 | SundeedLogger.debug("Number of queries in queue: \(self.sqlStatements.count)") 153 | guard let query = query else { 154 | return 155 | } 156 | if self.canExecute || force { 157 | var writeConnection = self.connection(write: true) 158 | self.canExecute = false 159 | var statement: OpaquePointer? 160 | let prepare = sqlite3_prepare_v2(writeConnection, query, -1, &statement, nil) 161 | if prepare == SQLITE_OK { 162 | parameters?.enumerated().forEach({ (index, value) in 163 | switch value { 164 | case .text(let value): 165 | if let value { 166 | sqlite3_bind_text(statement, Int32(index+1), 167 | value, -1, self.SQLITE_TRANSIENT) 168 | } else { 169 | sqlite3_bind_null(statement, Int32(index+1)) 170 | } 171 | case .integer(let value): 172 | if let value { 173 | sqlite3_bind_int(statement, Int32(index+1), Int32(value)) 174 | } else { 175 | sqlite3_bind_null(statement, Int32(index+1)) 176 | } 177 | case .double(let value): 178 | if let value { 179 | sqlite3_bind_double(statement, Int32(index+1), value) 180 | } else { 181 | sqlite3_bind_null(statement, Int32(index+1)) 182 | } 183 | case .blob(let data): 184 | if let data { 185 | sqlite3_bind_blob(statement, Int32(index+1), 186 | NSData(data: data).bytes, Int32(NSData(data: data).length), self.SQLITE_TRANSIENT) 187 | } else { 188 | sqlite3_bind_null(statement, Int32(index+1)) 189 | } 190 | } 191 | }) 192 | if sqlite3_step(statement) == SQLITE_DONE { 193 | sqlite3_finalize(statement) 194 | } else { 195 | let error = String(cString: sqlite3_errmsg(writeConnection)) 196 | SundeedLogger.debug(error) 197 | sqlite3_finalize(statement) 198 | self.insertStatement(query, parameters) 199 | } 200 | } else { 201 | if prepare != SQLITE_ERROR { 202 | sqlite3_finalize(statement) 203 | self.insertStatement(query, parameters) 204 | } else { 205 | let error = String(cString: sqlite3_errmsg(writeConnection)) 206 | SundeedLogger.debug(error) 207 | } 208 | } 209 | let combination = self.popStatement() 210 | self.closeConnection(writeConnection) 211 | writeConnection = nil 212 | statement = nil 213 | if let oldQuery = combination?.query { 214 | backgroundQueue.async(flags: .barrier) { 215 | self.execute(query: oldQuery, parameters: combination?.parameters, 216 | force: true) 217 | } 218 | } else { 219 | self.canExecute = true 220 | } 221 | } else { 222 | self.insertStatement(query, parameters) 223 | } 224 | } 225 | } 226 | private func insertStatement(_ query: String, _ parameters: [ParameterType]?) { 227 | self.sqlStatements.insert((query, parameters), at: 0) 228 | } 229 | private func popStatement() -> (query: String, parameters: [ParameterType]?)? { 230 | return self.sqlStatements.popLast() 231 | } 232 | private func removeAllStatements() { 233 | self.sqlStatements.removeAll() 234 | } 235 | func deleteDatabase() { 236 | queue.sync(flags: .barrier) { 237 | guard let fullDestinationPath = fullDestPath else { return } 238 | removeAllStatements() 239 | do { 240 | try fileManager.removeItem(at: fullDestinationPath) 241 | try deleteImagesDirectory() 242 | } catch { 243 | SundeedLogger.error(error) 244 | } 245 | } 246 | } 247 | private func deleteImagesDirectory() throws { 248 | guard let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask) 249 | .first else { 250 | throw SundeedQLiteError.cannotAccessDocumentDirectory 251 | } 252 | 253 | let directoryURL = documentsDirectoryURL.appendingPathComponent("SundeedQLite/Image", isDirectory: true) 254 | if fileManager.fileExists(atPath: directoryURL.path) { 255 | try fileManager.removeItem(at: directoryURL) 256 | } 257 | } 258 | private func createDatabaseIfNeeded() throws { 259 | guard let fullDestinationPath = fullDestPath else { return } 260 | let fullDestPathString = fullDestPath!.path 261 | if !fileManager.fileExists(atPath: fullDestPathString) { 262 | try "".write(to: fullDestinationPath, atomically: false, encoding: .utf8) 263 | setPragma() 264 | } 265 | } 266 | private func setPragma() { 267 | guard let conn = connection(write: true) else { 268 | SundeedLogger.debug("Could not open DB to set PRAGMA") 269 | return 270 | } 271 | 272 | var errmsg: UnsafeMutablePointer? 273 | let sql = "PRAGMA journal_mode = WAL;" 274 | if sqlite3_exec(conn, sql, nil, nil, &errmsg) != SQLITE_OK { 275 | if let e = errmsg { 276 | SundeedLogger.debug("PRAGMA error: \(String(cString: e))") 277 | sqlite3_free(errmsg) 278 | } else if let cmsg = sqlite3_errmsg(conn) { 279 | SundeedLogger.debug("PRAGMA failed: \(String(cString: cmsg))") 280 | } 281 | } else { 282 | SundeedLogger.debug("WAL mode set") 283 | } 284 | closeConnection(conn) 285 | } 286 | } 287 | --------------------------------------------------------------------------------