├── _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 |
--------------------------------------------------------------------------------