├── .editorconfig ├── .github ├── _support │ ├── Package.swift │ └── Package@swift-5.swift └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── CONTRIBUTING.md ├── LICENSE ├── Lighter.json ├── Makefile ├── Package.swift ├── Package@swift-5.10.swift ├── Package@swift-5.7.swift ├── Package@swift-5.8.swift ├── Package@swift-5.9.swift ├── Package@swift-5.swift ├── Package@swift-6.swift ├── Plugins ├── Enlighter │ ├── Enlighter.swift │ ├── EnlighterGroup.swift │ ├── EnlighterTargetConfig.swift │ ├── README.md │ └── XcodeSPMCompat.swift ├── GenerateCodeForSQLite │ ├── Arguments.swift │ ├── EnlighterGroup.swift │ ├── EnlighterTargetConfig.swift │ ├── GenerateCodeForSQLite.swift │ ├── README.md │ └── XcodeSPMCompat.swift ├── Libraries │ ├── LighterCodeGenAST │ │ ├── Generation │ │ │ ├── CodeGenerator.swift │ │ │ ├── GenExpressions.swift │ │ │ ├── GenExtensions.swift │ │ │ ├── GenFunctions.swift │ │ │ ├── GenLiterals.swift │ │ │ ├── GenStatements.swift │ │ │ ├── GenStructures.swift │ │ │ ├── GenTypes.swift │ │ │ ├── GenUnit.swift │ │ │ └── ReservedWords.swift │ │ ├── Nodes │ │ │ ├── CompilationUnit.swift │ │ │ ├── ComputedPropertyDefinition.swift │ │ │ ├── Expression.swift │ │ │ ├── Extension.swift │ │ │ ├── FunctionComment.swift │ │ │ ├── FunctionDeclaration.swift │ │ │ ├── FunctionDefinition.swift │ │ │ ├── FunctionParameter.swift │ │ │ ├── GenericConstraint.swift │ │ │ ├── Literal.swift │ │ │ ├── Statement.swift │ │ │ ├── TypeComment.swift │ │ │ ├── TypeDefinition.swift │ │ │ └── TypeReference.swift │ │ └── README.md │ └── LighterGeneration │ │ ├── CodeGenerator.swift │ │ ├── GenModel │ │ ├── DatabaseInfo.swift │ │ ├── EntityInfo.swift │ │ ├── EntitySQLStatements.swift │ │ ├── Fancyfier.swift │ │ ├── Property.swift │ │ ├── Relationships.swift │ │ └── SchemaInit.swift │ │ ├── LighterAPI.swift │ │ ├── LighterConfiguration │ │ ├── ASTGeneratorConfig.swift │ │ ├── CodeGeneratorConfig.swift │ │ ├── ConfigFile.swift │ │ ├── EmbeddedLighter.swift │ │ ├── FancyfierConfig.swift │ │ ├── JSONUtil.swift │ │ ├── LighterConfiguration.swift │ │ └── README.md │ │ ├── README.md │ │ ├── RecordGeneration │ │ ├── EnlighterASTGenerator.swift │ │ ├── GenerateCombinedFile.swift │ │ ├── GenerateDatabaseStruct.swift │ │ ├── GenerateDatabaseSupport.swift │ │ ├── GenerateDefaultValues.swift │ │ ├── GenerateOptionalHelpers.swift │ │ ├── GeneratePropertyType.swift │ │ ├── GenerateRawFunctions.swift │ │ ├── GenerateRawRelshipFunctions.swift │ │ ├── GenerateRecordRelshipFunctions.swift │ │ ├── GenerateRecordStatementBind.swift │ │ ├── GenerateRecordStatementInit.swift │ │ ├── GenerateRecordStructure.swift │ │ ├── GenerateRecordSwiftMatcher.swift │ │ └── GenerateSchemaStructure.swift │ │ ├── SchemaLoader.swift │ │ ├── Utilities │ │ ├── CamelCase.swift │ │ ├── ConcurrencyCompat.swift │ │ ├── GenerateSchemaSwiftInit.swift │ │ ├── Pluralize.swift │ │ └── SQLGeneration.swift │ │ └── VariadicGeneration │ │ ├── FunctionGenerator.swift │ │ ├── GenerateInsertFunctions.swift │ │ ├── GenerateInternalVariadics.swift │ │ ├── GenerateSelectFunctions.swift │ │ └── GenerateUpdateFunctions.swift ├── Tools │ ├── GenerateInternalVariadics │ │ ├── README.md │ │ └── main.swift │ └── sqlite2swift │ │ ├── Arguments.swift │ │ ├── ExitCodes.swift │ │ ├── README.md │ │ ├── SQLite2Swift.swift │ │ └── main.swift └── WriteInternalVariadics │ ├── README.md │ └── WriteInternalVariadics.swift ├── README.md ├── Sources ├── Lighter │ ├── ConnectionHandlers │ │ ├── SQLConnectionHandler.swift │ │ ├── SimplePool.swift │ │ └── UnsafeReuse.swift │ ├── Database │ │ ├── SQLDatabase.swift │ │ ├── SQLDatabaseCreation.swift │ │ └── SQLDatabaseTesting.swift │ ├── Expression │ │ ├── SQLBuilder.swift │ │ ├── SQLExpression.swift │ │ └── SQLInterpolation.swift │ ├── Lighter.docc │ │ ├── Configuration.md │ │ ├── FAQ.md │ │ ├── GettingStarted.md │ │ ├── Lighter.md │ │ ├── LighterAPI.md │ │ ├── Linux.md │ │ ├── Manual.md │ │ ├── Mapping.md │ │ ├── Migrations.md │ │ ├── Northwind.md │ │ ├── Performance.md │ │ ├── SQLiteAPI.md │ │ ├── Troubleshooting.md │ │ └── Who.md │ ├── Operations │ │ ├── GeneratedVariadicOperations.swift │ │ ├── SQLDatabaseAsyncChangeOperations.swift │ │ ├── SQLDatabaseAsyncFetchOperations.swift │ │ ├── SQLDatabaseAsyncOperations.swift │ │ ├── SQLDatabaseChangeOperations.swift │ │ ├── SQLDatabaseFetchOperations.swift │ │ ├── SQLDatabaseOperations.swift │ │ ├── SQLPragmaOperations.swift │ │ ├── SQLRecordAsyncFetchOperations.swift │ │ ├── SQLRecordFetchOperations.swift │ │ ├── SQLRecordFilterOperations.swift │ │ └── SQLRecordForeignKeyOperations.swift │ ├── Predicates │ │ ├── PredicateOperators.swift │ │ ├── SQLColumnComparisonPredicate.swift │ │ ├── SQLColumnValuePredicate.swift │ │ ├── SQLColumnValueRangePredicate.swift │ │ ├── SQLColumnValueSetPredicate.swift │ │ ├── SQLCompoundPredicate.swift │ │ ├── SQLInterpolatedPredicate.swift │ │ ├── SQLNotPredicate.swift │ │ ├── SQLPredicate.swift │ │ ├── SQLSortOrder.swift │ │ └── SQLTruePredicate.swift │ ├── Schema │ │ ├── SQLColumn.swift │ │ ├── SQLEntitySchema.swift │ │ ├── SQLForeignKeyColumn.swift │ │ ├── SQLRecord.swift │ │ ├── SQLSwiftMatchableSchema.swift │ │ ├── SQLValueChanges.swift │ │ └── SQLiteValueType.swift │ ├── Transactions │ │ ├── SQLChangeTransaction.swift │ │ ├── SQLDatabaseTransaction.swift │ │ ├── SQLTransaction.swift │ │ ├── SQLTransactionAsync.swift │ │ └── SQLTransactionType.swift │ └── Utilities │ │ ├── LighterError.swift │ │ ├── OptionalCString.swift │ │ ├── SQLError.swift │ │ └── SendableKeyPath.swift ├── SQLite3-Linux │ ├── README.md │ ├── module.modulemap │ └── shim.h └── SQLite3Schema │ ├── CatalogObject.swift │ ├── Column.swift │ ├── DataTypes.swift │ ├── ForeignKey.swift │ ├── README.md │ ├── Schema.swift │ └── TableOrView.swift └── Tests ├── CodeGenASTTests ├── BuilderTests.swift ├── Fixtures.swift └── GenerationTests.swift ├── ContactsDatabaseTests ├── ContactsDatabaseTests.swift └── contacts-create.sql ├── EntityGenTests ├── ASTDatabaseStructGenerationTests.swift ├── ASTRawFunctionGenerationTests.swift ├── ASTRawRelshipFunctionGenerationTests.swift ├── ASTRecordBindGenerationTests.swift ├── ASTRecordInitGenerationTests.swift ├── ASTRecordMatcherGenerationTests.swift ├── ASTRecordRelshipGenerationTests.swift ├── ASTRecordSchemaGenerationTests.swift ├── ASTRecordStructGenerationTests.swift ├── EntityGenTests.swift ├── FancifierTests.swift ├── Fixtures.swift └── PluralizeTests.swift ├── FiveThirtyEightTests └── FiveThirtyEightTests.swift ├── LighterOperationGenTests ├── FetchOperationsTests.swift ├── InsertOperationsTests.swift └── UpdateOperationsTests.swift └── NorthwindTests └── NorthwindTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | tab_width = 8 5 | max_line_length = 80 6 | trim_trailing_whitespace = false 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/_support/Package@swift-5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | var package = Package( 6 | name: "Lighter", 7 | 8 | platforms: [ .macOS(.v10_14), .iOS(.v12) ], 9 | 10 | products: [ 11 | .library(name: "Lighter", targets: [ "Lighter" ]), 12 | .library(name: "SQLite3Schema", targets: [ "SQLite3Schema" ]), 13 | .executable(name: "sqlite2swift", targets: [ "sqlite2swift" ]) 14 | ], 15 | 16 | targets: [ 17 | // A small library used to fetch schema information from SQLite3 databases. 18 | .target(name: "SQLite3Schema", exclude: [ "README.md" ]), 19 | 20 | // Lighter is a shared lib providing common protocols used by Enlighter 21 | // generated models and such. 22 | // Note that Lighter isn't that useful w/o code generation (i.e. as a 23 | // standalone lib). 24 | .target(name: "Lighter"), 25 | 26 | 27 | // MARK: - Plugin Support 28 | 29 | // The CodeGenAST is a small and hacky helper lib that can format/render 30 | // Swift source code. 31 | .target(name : "LighterCodeGenAST", 32 | path : "Plugins/Libraries/LighterCodeGenAST", 33 | exclude : [ "README.md" ]), 34 | 35 | // This library contains all the code generation, to be used by different 36 | // clients. 37 | .target(name : "LighterGeneration", 38 | dependencies : [ "LighterCodeGenAST", "SQLite3Schema" ], 39 | path : "Plugins/Libraries/LighterGeneration", 40 | exclude : [ "README.md", "LighterConfiguration/README.md" ]), 41 | 42 | 43 | // MARK: - Tests 44 | 45 | .testTarget(name: "CodeGenASTTests", dependencies: [ "LighterCodeGenAST" ]), 46 | .testTarget(name: "EntityGenTests", dependencies: [ "LighterGeneration" ]), 47 | .testTarget(name: "LighterOperationGenTests", 48 | dependencies: [ "LighterGeneration" ]), 49 | 50 | 51 | // MARK: - sqlite2swift 52 | 53 | .target(name : "sqlite2swift", 54 | dependencies : [ "LighterGeneration" ], 55 | path : "Plugins/Tools/sqlite2swift", 56 | exclude : [ "README.md" ]), 57 | 58 | 59 | // MARK: - Internal Tool for Generating Variadics 60 | 61 | .target(name : "GenerateInternalVariadics", 62 | dependencies : [ "LighterCodeGenAST", "LighterGeneration" ], 63 | path : "Plugins/Tools/GenerateInternalVariadics", 64 | exclude : [ "README.md" ]), 65 | 66 | 67 | // MARK: - Environment specific tests 68 | .testTarget(name: "FiveThirtyEightTests", 69 | dependencies: [ "LighterGeneration" ]), 70 | .testTarget(name: "NorthwindTests", 71 | dependencies: [ "LighterGeneration" ]) 72 | ] 73 | ) 74 | 75 | #if !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) 76 | package.products += [ .library(name: "SQLite3", targets: [ "SQLite3" ]) ] 77 | package.targets += [ 78 | .systemLibrary(name: "SQLite3", 79 | path: "Sources/SQLite3-Linux", 80 | providers: [ .apt(["libsqlite3-dev"]) ]) 81 | ] 82 | package.targets 83 | .first(where: { $0.name == "SQLite3Schema" })? 84 | .dependencies.append("SQLite3") 85 | package.targets 86 | .first(where: { $0.name == "Lighter" })? 87 | .dependencies.append("SQLite3") 88 | #endif // not-Darwin 89 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | image: 14 | - swift:5.9.2-focal 15 | - swift:5.10-jammy 16 | - swift:6.0-noble 17 | container: ${{ matrix.image }} 18 | steps: 19 | - name: Install SQLite 20 | run: | 21 | apt-get -qq update 22 | apt-get -y -qq install libsqlite3-dev 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | - name: Install Override Package.swift 26 | run: cp .github/_support/Package.swift .github/_support/Package\@swift-5.swift . 27 | - name: Build Swift Debug Package 28 | run: swift build -c debug 29 | - name: Build Swift Release Package 30 | run: swift build -c release 31 | - name: Run Tests 32 | run: swift test 33 | nextstep: 34 | runs-on: macos-latest 35 | steps: 36 | - name: Select latest available Xcode 37 | uses: maxim-lobanov/setup-xcode@v1.5.1 38 | with: 39 | xcode-version: latest 40 | - name: Checkout Repository 41 | uses: actions/checkout@v4 42 | - name: Log Swift Version 43 | run: swift --version 44 | - name: Install Override Package.swift 45 | run: cp .github/_support/Package.swift .github/_support/Package\@swift-5.swift . 46 | - name: Build Swift Debug Package 47 | run: swift build -c debug 48 | - name: Build Swift Release Package 49 | run: swift build -c release 50 | - name: Run Tests 51 | run: swift test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .swiftpm 11 | TestGeneration.txt 12 | .docker.build 13 | .DS_Store 14 | 15 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ Lighter ] 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to ZeeZide GmbH and the community, and agree by submitting the 5 | patch that your contributions are licensed under the Apache 2.0 license. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ZeeZide GmbH / Helge Heß 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Lighter.json: -------------------------------------------------------------------------------- 1 | { 2 | "__doc__": "Configuration used for the manual, builtin codegen.", 3 | 4 | "databaseExtensions" : [ "sqlite3", "db", "sqlite" ], 5 | "sqlExtensions" : [ "sql" ], 6 | 7 | "CodeStyle": { 8 | "functionCommentStyle" : "**", 9 | "indent" : " ", 10 | "lineLength" : 80 11 | }, 12 | 13 | "EmbeddedLighter": { 14 | "selects": { 15 | "syncYield" : "none", 16 | "syncArray" : { "columns": 6, "sorts": 2 }, 17 | "asyncArray" : { "columns": 6, "sorts": 2 } 18 | }, 19 | "updates": { 20 | "keyBased" : 6, 21 | "predicateBased" : 6 22 | }, 23 | "inserts": 6 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | # local config 4 | SWIFT_BUILD=swift build 5 | SWIFT_CLEAN=swift package clean 6 | SWIFT_BUILD_DIR=.build 7 | SWIFT_TEST=swift test 8 | CONFIGURATION=debug 9 | DOCKER=/usr/local/bin/docker 10 | 11 | # docker config (5.6.1 seems the first w/ aarch64) 12 | #SWIFT_BUILD_IMAGE="swift:5.6.2-focal" 13 | #SWIFT_BUILD_IMAGE=swift:5.9.0-focal 14 | SWIFT_BUILD_IMAGE=swift:5.10.1-noble 15 | DOCKER_BUILD_DIR=".docker$(SWIFT_BUILD_DIR)" 16 | SWIFT_DOCKER_BUILD_DIR="$(DOCKER_BUILD_DIR)/aarch64-unknown-linux/$(CONFIGURATION)" 17 | DOCKER_BUILD_PRODUCT="$(DOCKER_BUILD_DIR)/$(TOOL_NAME)" 18 | 19 | 20 | SWIFT_SOURCES=\ 21 | Sources/*/*.swift 22 | 23 | all: all-native 24 | 25 | all-native: 26 | $(SWIFT_BUILD) -c $(CONFIGURATION) 27 | 28 | # Cannot test in `release` configuration?! 29 | test: 30 | $(SWIFT_TEST) 31 | 32 | clean : 33 | $(SWIFT_CLEAN) 34 | # We have a different definition of "clean", might be just German 35 | # pickyness. 36 | rm -rf $(SWIFT_BUILD_DIR) 37 | 38 | 39 | # Test Linux Builds in Docker 40 | 41 | $(DOCKER_BUILD_PRODUCT): $(SWIFT_SOURCES) 42 | $(DOCKER) run --rm \ 43 | -v "$(PWD):/src" \ 44 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 45 | "$(SWIFT_BUILD_IMAGE)" \ 46 | bash -c 'apt-get update -qq && apt-get install -y -qq libsqlite3-dev 2>&1 >/dev/null && cd /src && swift build -c $(CONFIGURATION)' 47 | 48 | docker-all: $(DOCKER_BUILD_PRODUCT) 49 | 50 | docker-test: docker-all 51 | $(DOCKER) run --rm \ 52 | -v "$(PWD):/src" \ 53 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 54 | "$(SWIFT_BUILD_IMAGE)" \ 55 | bash -c 'apt-get update -qq && apt-get install -y -qq libsqlite3-dev 2>&1 >/dev/null && cd /src && swift test -c $(CONFIGURATION)' 56 | 57 | docker-clean: 58 | rm $(DOCKER_BUILD_PRODUCT) 59 | 60 | docker-distclean: 61 | rm -rf $(DOCKER_BUILD_DIR) 62 | 63 | distclean: clean docker-distclean 64 | -------------------------------------------------------------------------------- /Package@swift-5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | var package = Package( 6 | name: "Lighter", 7 | 8 | platforms: [ .macOS(.v10_14), .iOS(.v12) ], 9 | 10 | products: [ 11 | .library(name: "Lighter", targets: [ "Lighter" ]), 12 | .library(name: "SQLite3Schema", targets: [ "SQLite3Schema" ]), 13 | .executable(name: "sqlite2swift", targets: [ "sqlite2swift" ]) 14 | ], 15 | 16 | targets: [ 17 | // A small library used to fetch schema information from SQLite3 databases. 18 | .target(name: "SQLite3Schema", exclude: [ "README.md" ]), 19 | 20 | // Lighter is a shared lib providing common protocols used by Enlighter 21 | // generated models and such. 22 | // Note that Lighter isn't that useful w/o code generation (i.e. as a 23 | // standalone lib). 24 | .target(name: "Lighter"), 25 | 26 | 27 | // MARK: - Plugin Support 28 | 29 | // The CodeGenAST is a small and hacky helper lib that can format/render 30 | // Swift source code. 31 | .target(name : "LighterCodeGenAST", 32 | path : "Plugins/Libraries/LighterCodeGenAST", 33 | exclude : [ "README.md" ]), 34 | 35 | // This library contains all the code generation, to be used by different 36 | // clients. 37 | .target(name : "LighterGeneration", 38 | dependencies : [ "LighterCodeGenAST", "SQLite3Schema" ], 39 | path : "Plugins/Libraries/LighterGeneration", 40 | exclude : [ "README.md", "LighterConfiguration/README.md" ]), 41 | 42 | 43 | // MARK: - Tests 44 | 45 | .testTarget(name: "CodeGenASTTests", dependencies: [ "LighterCodeGenAST" ]), 46 | .testTarget(name: "EntityGenTests", dependencies: [ "LighterGeneration" ]), 47 | .testTarget(name: "LighterOperationGenTests", 48 | dependencies: [ "LighterGeneration" ]), 49 | .testTarget(name: "ContactsDatabaseTests", dependencies: [ "Lighter" ], 50 | exclude: [ "contacts-create.sql" ]), 51 | 52 | 53 | // MARK: - sqlite2swift 54 | 55 | .target(name : "sqlite2swift", 56 | dependencies : [ "LighterGeneration" ], 57 | path : "Plugins/Tools/sqlite2swift", 58 | exclude : [ "README.md" ]), 59 | 60 | 61 | // MARK: - Internal Tool for Generating Variadics 62 | 63 | .target(name : "GenerateInternalVariadics", 64 | dependencies : [ "LighterCodeGenAST", "LighterGeneration" ], 65 | path : "Plugins/Tools/GenerateInternalVariadics", 66 | exclude : [ "README.md" ]), 67 | 68 | 69 | // MARK: - Environment specific tests 70 | .testTarget(name: "FiveThirtyEightTests", 71 | dependencies: [ "LighterGeneration" ]), 72 | .testTarget(name: "NorthwindTests", 73 | dependencies: [ "LighterGeneration" ]) 74 | ] 75 | ) 76 | 77 | #if !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) 78 | package.products += [ .library(name: "SQLite3", targets: [ "SQLite3" ]) ] 79 | package.targets += [ 80 | .systemLibrary(name: "SQLite3", 81 | path: "Sources/SQLite3-Linux", 82 | providers: [ .apt(["libsqlite3-dev"]) ]) 83 | ] 84 | package.targets 85 | .first(where: { $0.name == "SQLite3Schema" })? 86 | .dependencies.append("SQLite3") 87 | package.targets 88 | .first(where: { $0.name == "Lighter" })? 89 | .dependencies.append("SQLite3") 90 | #endif // not-Darwin 91 | -------------------------------------------------------------------------------- /Plugins/Enlighter/EnlighterTargetConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import struct Foundation.URL 7 | 8 | struct EnlighterTargetConfig { // that's all we need from Lighter.json 9 | 10 | let dbExtensions : Set 11 | let extensions : Set 12 | let outputFile : String? 13 | 14 | let verbose : Bool 15 | let configURL : URL? 16 | } 17 | -------------------------------------------------------------------------------- /Plugins/Enlighter/README.md: -------------------------------------------------------------------------------- 1 |

Enlighter (Build Tool Plugin) 2 | 4 |

5 | 6 | A SwiftPM build plugin that searches for SQLite databases and `.sql` files 7 | within the selected targets, and then generates Swift sources to use those 8 | databases. 9 | 10 | The databases can be copied as a resource, but don't have to be. This looks at 11 | all database/SQL files, unless contained in a `NoSQL` directory. 12 | 13 | The generated Swift database types are grouped by the stem within a target. 14 | E.g. a target could contain: 15 | 16 | ContactsDB.01.create.sql 17 | ContactsDB.02.setup-indices.sql 18 | ContactsDB.03.migrate-to-v3.sql 19 | ContactsDB.04.migrate-to-v4.sql 20 | TodosDB.db 21 | 22 | This will create two Swift database types: `ContactsDB` and `TodosDB`. 23 | Within the group the files are sorted by name and executed in that order to 24 | produces the actual database. 25 | 26 | The function can be configured using a `Lighter.json` config file in the 27 | package root. 28 | 29 | Alongside this there is the `GenerateCodeForSQLite` command plugin, which can 30 | manually generate wrapper code into the `Sources/target/dbname.swift` file. 31 | 32 | This plugin generates into a derived-data/sources directory used by Xcode or 33 | SPM. 34 | 35 | Example use in a SPM target: 36 | ```swift 37 | .target(name : "ContactsTestDB", 38 | dependencies : [ "Lighter" ], 39 | resources : [ .copy("ContactsDB.sqlite3") ], 40 | plugins : [ "Enlighter" ]) 41 | ``` 42 | 43 | 44 | ### Who 45 | 46 | Lighter is brought to you by 47 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 48 | We like feedback, GitHub stars, cool contract work, 49 | presumably any form of praise you can think of. 50 | -------------------------------------------------------------------------------- /Plugins/GenerateCodeForSQLite/Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | struct Arguments { 7 | 8 | let verbose : Bool 9 | let targets : [ String ] 10 | 11 | init(_ arguments: [ String ]) { 12 | 13 | func extractValuesOfOption(_ option: String, from args: inout [ String ]) 14 | -> [ String ] 15 | { 16 | var values = [ String ]() 17 | let long = "--" + option 18 | while let idx = args.firstIndex(of: long) { 19 | guard (idx + 1) < args.count else { 20 | print("ERROR: trailing `\(long)` option") 21 | break 22 | } 23 | values.append(args.remove(at: idx + 1)) 24 | args.remove(at: idx) 25 | } 26 | return values 27 | } 28 | 29 | var args = arguments 30 | targets = extractValuesOfOption("target", from: &args) 31 | 32 | if let idx = args.firstIndex(of: "--verbose") { 33 | verbose = true 34 | args.remove(at: idx) 35 | } 36 | else { 37 | #if DEBUG // remove me 38 | verbose = true 39 | #else 40 | verbose = false 41 | #endif 42 | } 43 | 44 | assert(args.isEmpty, "other options left: \(args)") 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Plugins/GenerateCodeForSQLite/EnlighterTargetConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import struct Foundation.URL 7 | 8 | struct EnlighterTargetConfig { // that's all we need from Lighter.json 9 | 10 | let dbExtensions : Set 11 | let extensions : Set 12 | let outputFile : String? 13 | 14 | let verbose : Bool 15 | let configURL : URL? 16 | } 17 | -------------------------------------------------------------------------------- /Plugins/GenerateCodeForSQLite/README.md: -------------------------------------------------------------------------------- 1 |

Generate Code for SQLite (Command Plugin) 2 | 4 |

5 | 6 | A SwiftPM command plugin that searches for SQLite databases and `.sql` files 7 | within the selected targets, and then generates Swift sources to use those 8 | databases. 9 | 10 | The databases can be copied as a resource, but don't have to be. This looks at 11 | all database/SQL files, unless contained in a `NoSQL` directory. 12 | 13 | The generated Swift database types are grouped by the stem within a target. 14 | E.g. a target could contain: 15 | 16 | ContactsDB.01.create.sql 17 | ContactsDB.02.setup-indices.sql 18 | ContactsDB.03.migrate-to-v3.sql 19 | ContactsDB.04.migrate-to-v4.sql 20 | TodosDB.db 21 | 22 | This will create two Swift database types: `ContactsDB` and `TodosDB`. 23 | Within the group the files are sorted by name and executed in that order to 24 | produces the actual database. 25 | 26 | Command plugins can be triggered from the "File / Packages" menu, 27 | from the context menu on the package entry in the "Project Navigator", 28 | or on the commandline, e.g.: 29 | ```bash 30 | swift package plugin \ 31 | --allow-writing-to-package-directory \ 32 | sqlite2swift \ 33 | --target ContactsTestDB 34 | ``` 35 | Note: The commandline name is `sqlite2swift`. 36 | 37 | The function can be configured using a `Lighter.json` config file in the 38 | package root. 39 | 40 | Alongside this there is the `Enlighter` build tool plugin, which can generate 41 | the wrapper code automatically. 42 | This command plugin tool does it manually and directly generates into the 43 | `Sources/target/dbname.swift` file! 44 | 45 | 46 | ### Who 47 | 48 | Lighter is brought to you by 49 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 50 | We like feedback, GitHub stars, cool contract work, 51 | presumably any form of praise you can think of. 52 | -------------------------------------------------------------------------------- /Plugins/GenerateCodeForSQLite/XcodeSPMCompat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | import PackagePlugin 7 | 8 | #if canImport(XcodeProjectPlugin) 9 | // Abstract those as protocols is a little harder than expected as `Target` 10 | // in SPM is also a protocol. 11 | 12 | import XcodeProjectPlugin 13 | #if compiler(>=6) && canImport(Foundation) 14 | import Foundation 15 | #endif 16 | 17 | extension XcodePluginContext { 18 | 19 | var package : XcodeProject { xcodeProject } 20 | } 21 | 22 | extension XcodeProject { 23 | 24 | func targets(named targetNames: [ String ]) throws -> [ XcodeTarget ] { 25 | self.targets.filter { targetNames.contains($0.name) } 26 | } 27 | } 28 | 29 | extension XcodeTarget { 30 | 31 | var name : String { product?.name ?? displayName } 32 | 33 | #if compiler(>=6) && canImport(Foundation) 34 | var directoryURL : URL { 35 | // heuristics, in Xcode the input files can come from anywhere 36 | var directories = [ URL ]() 37 | for file in inputFiles { 38 | let dir = file.url.deletingLastPathComponent() 39 | if !directories.contains(dir) { 40 | directories.append(dir) 41 | } 42 | } 43 | if directories.isEmpty { 44 | assertionFailure("Target has no input files!") 45 | return URL(fileURLWithPath: "/tmp/") 46 | } 47 | 48 | if directories.count == 1, let sole = directories.first { return sole } 49 | 50 | if let matching = directories.first(where: { $0.lastPathComponent == name }) 51 | { 52 | return matching 53 | } 54 | // give up and use first :-) 55 | return directories[0] 56 | } 57 | #else 58 | var directory : Path { 59 | // heuristics, in Xcode the input files can come from anywhere 60 | var directories = [ Path ]() 61 | for file in inputFiles { 62 | let dir = file.path.removingLastComponent() 63 | if !directories.contains(dir) { 64 | directories.append(dir) 65 | } 66 | } 67 | if directories.isEmpty { 68 | assertionFailure("Target has no input files!") 69 | return Path("/tmp/") 70 | } 71 | 72 | if directories.count == 1, let sole = directories.first { return sole } 73 | 74 | let match = "/" + name 75 | if let matching = directories.first(where: { $0.string.hasSuffix(match) }) { 76 | return matching 77 | } 78 | // give up and use first :-) 79 | return directories[0] 80 | } 81 | #endif 82 | } 83 | 84 | #endif // canImport(XcodeProjectPlugin) 85 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Generation/GenExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | public extension CodeGenerator { 7 | 8 | /// `extension SQLColumn { ... }` 9 | func generateExtension(_ value: Extension) { 10 | if !source.isEmpty { appendEOLIfMissing() } 11 | 12 | if let ( major, minor ) = value.minimumSwiftVersion { 13 | assert(major >= 5) 14 | writeln("#if swift(>=\(major).\(minor))") 15 | } 16 | if !value.requiredImports.isEmpty { 17 | writeln("#if " + value.requiredImports.map { "canImport(\($0))" } 18 | .joined(separator: " && ")) 19 | } 20 | 21 | if value.requiredImports.contains("_Concurrency") { 22 | writeln("@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)") 23 | } 24 | 25 | appendIndent() 26 | if value.public && value.conformances.isEmpty { append("public ") } 27 | append("extension ") 28 | append(string(for: value.extendedType)) 29 | 30 | if !value.conformances.isEmpty { 31 | append(configuration.typeConformanceSeparator) // " : " 32 | append(value.conformances.map(string(for:)) 33 | .joined(separator: configuration.identifierListSeparator)) 34 | } 35 | 36 | if !value.genericConstraints.isEmpty { 37 | let constraints = value.genericConstraints.map({ string(for: $0) }) 38 | .joined(separator: configuration.identifierListSeparator) 39 | appendEOL() 40 | indent { 41 | writeln("where \(constraints)") 42 | } 43 | writeln("{") 44 | } 45 | else { 46 | append(" {") 47 | appendEOL() 48 | } 49 | 50 | indent { 51 | for typeDefinition in value.typeDefinitions { 52 | writeln() 53 | generateTypeDefinition(typeDefinition, omitPublic: value.public) 54 | } 55 | 56 | var lastHadComment = false 57 | if !value.typeVariables.isEmpty { writeln() } 58 | for variable in value.typeVariables { 59 | if lastHadComment { writeln() } 60 | generateInstanceVariable(variable, static: true) 61 | lastHadComment = variable.comment != nil 62 | } 63 | 64 | for function in value.typeFunctions { 65 | writeln() 66 | generateFunctionDefinition(function, omitPublic: value.public, 67 | static: true) 68 | } 69 | 70 | for function in value.functions { 71 | writeln() 72 | generateFunctionDefinition(function, omitPublic: value.public) 73 | } 74 | } 75 | writeln("}") 76 | 77 | if !value.requiredImports.isEmpty { 78 | writeln("#endif // required canImports") 79 | } 80 | if let ( major, minor ) = value.minimumSwiftVersion { 81 | writeln("#endif // swift(>=\(major).\(minor))") 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Generation/GenTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | public extension CodeGenerator { 7 | 8 | /// Returns a Swift string for the given type. 9 | /// E.g. ``TypeReference/void`` becomes `"Void"`. 10 | func string(for typeRef: TypeReference) -> String { 11 | switch typeRef { 12 | case .void : return "Void" 13 | case .optional(let t) : return "\(string(for: t))?" 14 | case .name (let name) : return name 15 | case .some (let name) : return "some \(name)" 16 | case .array(let type) : return "[ \(string(for: type)) ]" 17 | case .inout(let type) : return "inout \(string(for: type))" 18 | 19 | case .qualifiedType(let baseName, let name): return "\(baseName).\(name)" 20 | 21 | case .keyPath(let fromType, let toType): 22 | return "KeyPath<\(string(for: fromType)), \(string(for: toType))>" 23 | 24 | case .closure(let escaping, let parameters, let doesThrow, let returns): 25 | let prefix = escaping ? "@escaping " : "" 26 | let ts = doesThrow ? "throws " : "" 27 | let res = string(for: returns) 28 | if parameters.isEmpty { return "\(prefix)() \(ts)-> \(res)" } 29 | return "\(prefix)( " 30 | + parameters.map({ string(for: $0) }).joined(separator: ", ") 31 | + " ) \(ts)-> \(res)" 32 | 33 | case .tuple(let names, let types): 34 | guard !types.isEmpty else { return "()" } // aka `Void 35 | var s = "( " 36 | var isFirst = true 37 | for ( idx, type ) in types.enumerated() { 38 | if isFirst { isFirst = false } else { s += ", " } 39 | if let name = idx < names.count ? names[idx] : nil { 40 | s += "\(name): " 41 | s += string(for: type) 42 | } 43 | } 44 | s += " )" 45 | return s 46 | } 47 | } 48 | 49 | /// Returns a Swift string for the given generic constraint. 50 | /// E.g. `"C: SQLColumn"`. 51 | func string(for constraint: GenericConstraint) -> String { 52 | switch constraint { 53 | 54 | case .conformance(let name, let type): 55 | return "\(name): \(string(for: type))" 56 | 57 | case .equal(let name, let type): 58 | return "\(name) == \(string(for: type))" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Generation/GenUnit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | public extension CodeGenerator { 7 | 8 | func generateUnit(_ unit: CompilationUnit) { 9 | for imp in unit.reexports { 10 | writeln("@_exported import \(imp)") 11 | } 12 | for imp in unit.imports { 13 | writeln("import \(imp)") 14 | } 15 | 16 | for function in unit.functions { 17 | writeln() 18 | generateFunctionDefinition(function) 19 | } 20 | 21 | for typeDefinition in unit.typeDefinitions { 22 | writeln() 23 | generateTypeDefinition(typeDefinition) 24 | } 25 | 26 | for ext in unit.extensions { 27 | writeln() 28 | generateExtension(ext) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Generation/ReservedWords.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | // hm, this doesn't actually work: 7 | // ``` 8 | // let `try!` = 10 9 | // ``` 10 | 11 | let SwiftReservedWords : Set = [ 12 | "import", 13 | "let", "var", "static", 14 | "public", "private", "fileprivate", "internal", "open", 15 | "class", "struct", "extension", "protocol", "enum", "case", "default", 16 | "func", "throws", "rethrows", "mutating", "nonmutating", 17 | "init", "deinit", "inout", "optional", "override", 18 | "try", 19 | "operator", "infix", "subscript", "defer", 20 | "break", "fallthrough", "indirect", "lazy", 21 | "for", "do", "while", "repeat", "continue", "return", "in", "where", 22 | "if", "guard", "else", "switch", "catch", "throw", 23 | "true", "false", "nil", "self", 24 | "as", "is", 25 | "typealias", "associatedtype", 26 | "associativity", "dynamic", "convenience", "required", "final", 27 | "didSet", "willSet", "get", "set", 28 | "weak", "unowned", 29 | "actor", "async", "await", "sending" 30 | ] 31 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/CompilationUnit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A Swift file, containing structs, funcs, extensions and more. 8 | * 9 | * It does intentionally not support arbitrary order of things, but groups 10 | * stuff by type. 11 | */ 12 | public struct CompilationUnit { 13 | 14 | /// The (file)name of the unit. 15 | public let name : String 16 | 17 | /// A set of imports, e.g. `[ Foundation, SQLite3 ]` 18 | public var imports : [ String ] 19 | /// A set of imports that are re-exported (`@_exported import Lighter`) 20 | public var reexports : [ String ] = [] 21 | 22 | /// The structures that are part of the unit. 23 | public var typeDefinitions : [ TypeDefinition ] 24 | /// The functions that are part of the unit. 25 | public var functions : [ FunctionDefinition ] 26 | /// The extensions that are part of the unit. 27 | public var extensions : [ Extension ] 28 | 29 | /// Initialize a new CompilationUnit, only name and extensions are required. 30 | public init(name : String, 31 | imports : [ String ] = [], 32 | typeDefinitions : [ TypeDefinition ] = [], 33 | functions : [ FunctionDefinition ] = [], 34 | extensions : [ Extension ] = []) 35 | { 36 | self.name = name 37 | self.imports = imports 38 | self.typeDefinitions = typeDefinitions 39 | self.functions = functions 40 | self.extensions = extensions 41 | } 42 | /// Initialize a new CompilationUnit, only name and extensions are required. 43 | public init(name : String, 44 | imports : [ String ] = [], 45 | structures : [ TypeDefinition ], // legacy compat 46 | functions : [ FunctionDefinition ] = [], 47 | extensions : [ Extension ] = []) 48 | { 49 | self.init(name: name, imports: imports, typeDefinitions: structures, 50 | functions: functions, extensions: extensions) 51 | } 52 | } 53 | 54 | 55 | // MARK: - Convenience 56 | 57 | public extension CompilationUnit { 58 | 59 | /// Add an ``Extension`` to a compilation unit if it actually contains 60 | /// something. 61 | mutating func addIfNotEmpty(_ ext: Extension?) { 62 | guard let ext = ext, !ext.isEmpty else { return } 63 | extensions.append(ext) 64 | } 65 | } 66 | 67 | #if swift(>=5.5) 68 | extension CompilationUnit: Sendable {} 69 | #endif 70 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/ComputedPropertyDefinition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * An AST node that represents a computed property. 8 | * 9 | * Like: 10 | * ```swift 11 | * var _allColumns : [ any SQLColumn ] { id, street, name } 12 | * ``` 13 | */ 14 | public struct ComputedPropertyDefinition { 15 | 16 | /// Whether the definition is `@inlinable` (included in the module header). 17 | public var inlinable : Bool 18 | /// A comment for the property. 19 | public var comment : String? 20 | 21 | /// Is the property public? 22 | public let `public` : Bool 23 | 24 | /// The name of the property, e.g. `_allColumns`. 25 | public let name : String // e.g. `select` 26 | /// The type of the property, e.g. `.integer` 27 | public let type : TypeReference 28 | 29 | /// The ``Statement``s for the property getter. 30 | public var statements : [ Statement ] 31 | /// The ``Statement``s for the property setter. 32 | public var setStatements : [ Statement ] 33 | 34 | /// If set, the property is wrapped in an `#if swift(>=major.minor)`. 35 | public var minimumSwiftVersion : ( major: Int, minor: Int )? 36 | 37 | /// Initialize a new ComputedProperty AST node. 38 | public init(`public` : Bool = true, 39 | name : String, 40 | type : TypeReference, 41 | statements : [ Statement ], 42 | setStatements : [ Statement ] = [], 43 | comment : String? = nil, 44 | inlinable : Bool = false, 45 | minimumSwiftVersion : ( major: Int, minor: Int )? = nil) 46 | { 47 | self.`public` = `public` 48 | self.name = name 49 | self.type = type 50 | 51 | self.statements = statements 52 | self.setStatements = setStatements 53 | self.comment = comment 54 | self.inlinable = inlinable 55 | self.minimumSwiftVersion = minimumSwiftVersion 56 | } 57 | } 58 | 59 | 60 | // MARK: - Convenience 61 | 62 | public extension ComputedPropertyDefinition { 63 | 64 | /// Initialize a new ComputedProperty AST node with just getters. 65 | static func `var`(`public`: Bool = true, inlinable: Bool = true, 66 | _ name: String, 67 | _ type: TypeReference, 68 | comment: String? = nil, 69 | _ statements: Statement...) -> Self 70 | { 71 | .init(public: `public`, name: name, type: type, 72 | statements: statements, setStatements: [], 73 | comment: comment, inlinable: inlinable) 74 | } 75 | 76 | /// Initialize a new ComputedProperty AST node with setters. 77 | static func `var`(`public`: Bool = true, inlinable: Bool = true, 78 | _ name: String, 79 | _ type: TypeReference, 80 | set : [ Statement ], 81 | get : [ Statement ], 82 | comment: String? = nil) -> Self 83 | { 84 | .init(public: `public`, name: name, type: type, 85 | statements: get, setStatements: set, 86 | comment: comment, inlinable: inlinable) 87 | } 88 | } 89 | 90 | #if swift(>=5.5) 91 | extension ComputedPropertyDefinition : Sendable {} 92 | #endif 93 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * An AST node representing a Swift extension. 8 | */ 9 | public struct Extension { 10 | 11 | // MARK: - Header 12 | 13 | /// Is the extension public? 14 | public var `public` : Bool 15 | /// What type is the extension on? E.g. `Person`. 16 | public var extendedType : TypeReference 17 | /// The types the structure conforms to, e.g. `SQLTableRecord`. 18 | public var conformances : [ TypeReference ] 19 | /// Generic constraints to make the extension apply, 20 | /// e.g. `where RecordTypes == MyDatabase.RecordTypes` 21 | public var genericConstraints : [ GenericConstraint ] 22 | 23 | // MARK: - Types 24 | 25 | /// The structures added to the ``extendedType``. 26 | public var typeDefinitions : [ TypeDefinition ] 27 | 28 | // MARK: - Variables 29 | 30 | /// The type variables of the structure (i.e. `static let xyz` etc). 31 | public var typeVariables : [ TypeDefinition.InstanceVariable ] 32 | 33 | // MARK: - Functions 34 | 35 | /// Type functions, i.e. `static` ones, added by the extension. 36 | public var typeFunctions : [ FunctionDefinition ] 37 | /// Regular "instance" functions added by the extension. 38 | public var functions : [ FunctionDefinition ] 39 | 40 | /// If set, the extension is wrapped in an `#if swift(>=major.minor)`. 41 | public var minimumSwiftVersion : ( major: Int, minor: Int )? 42 | /// If set, the extension is wrapped in an `#if canImport(Module)` for each 43 | /// element. 44 | public var requiredImports = [ String ]() 45 | 46 | /// Initialize a new extension AST node. 47 | public init(extendedType : TypeReference, 48 | conformances : [ TypeReference ] = [], 49 | `public` : Bool = true, 50 | genericConstraints : [ GenericConstraint ] = [], 51 | typeDefinitions : [ TypeDefinition ] = [], 52 | typeVariables : [ TypeDefinition.InstanceVariable ] = [], 53 | typeFunctions : [ FunctionDefinition ] = [], 54 | functions : [ FunctionDefinition ] = [], 55 | minimumSwiftVersion : ( major: Int, minor: Int )? = nil, 56 | requiredImports : [ String ] = []) 57 | { 58 | self.public = `public` && conformances.isEmpty 59 | self.extendedType = extendedType 60 | self.conformances = conformances 61 | self.typeDefinitions = typeDefinitions 62 | self.typeVariables = typeVariables 63 | self.typeFunctions = typeFunctions 64 | self.functions = functions 65 | self.genericConstraints = genericConstraints 66 | self.minimumSwiftVersion = minimumSwiftVersion 67 | self.requiredImports = requiredImports 68 | } 69 | 70 | @inlinable 71 | public var isEmpty : Bool { 72 | functions.isEmpty && typeDefinitions.isEmpty && typeFunctions.isEmpty 73 | } 74 | } 75 | 76 | #if swift(>=5.5) 77 | extension Extension: Sendable {} 78 | #endif 79 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/FunctionComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A comment for a function. 8 | * 9 | * Has all the function specific things, like parameters and such. 10 | */ 11 | public struct FunctionComment: Equatable { 12 | 13 | /// A comment for a function parameter. 14 | public struct Parameter: Equatable { 15 | 16 | /// The name of the function parameter being documented. 17 | public var name : String 18 | /// The documentation for the parameter. 19 | public var info : String 20 | 21 | /// Initialize a function parameter comment. 22 | public init(name: String, info: String) { 23 | self.name = name 24 | self.info = info 25 | } 26 | } 27 | 28 | /// The introducing headline of the comment. 29 | public var headline : String 30 | /// A paragraph with more information about the function. 31 | public var info : String? 32 | /// A single example that will be emitted in triple-backticks. 33 | public var example : String? 34 | 35 | /// Documentation for the parameters of the function. 36 | public var parameters = [ Parameter ]() 37 | 38 | /// Whether the function throws errors. 39 | public var `throws` : Bool = false 40 | /// The documentation for the return value of the function. 41 | public var returnInfo : String? 42 | 43 | /// Initialize a new function comment AST node. 44 | public init(headline: String, info: String? = nil, example: String? = nil, 45 | parameters: [ Parameter ] = [], `throws`: Bool = false, 46 | returnInfo: String? = nil) 47 | { 48 | self.headline = headline 49 | self.info = info 50 | self.example = example 51 | self.parameters = parameters 52 | self.throws = `throws` 53 | self.returnInfo = returnInfo 54 | } 55 | } 56 | 57 | #if swift(>=5.5) 58 | extension FunctionComment : Sendable {} 59 | extension FunctionComment.Parameter : Sendable {} 60 | #endif 61 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/FunctionDefinition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A function definition is the ``FunctionDeclaration`` alongside the 8 | * ``Statement``s that make up the function. 9 | * 10 | * Plus extra annotations like ``inlinable`` and the ``comment``. 11 | */ 12 | public struct FunctionDefinition: Equatable { 13 | 14 | /// Whether the definition is `@inlinable` (included in the module header). 15 | public var inlinable : Bool 16 | // TBD: decl or def? 17 | 18 | /// Whether the result is `@discardableResult`. 19 | public var discardableResult : Bool 20 | 21 | /// A comment for the function. 22 | public var comment : FunctionComment? 23 | 24 | /// The type (declaration) of the function. Has the parameter types, the 25 | /// return types, whether it throws etc. 26 | public let declaration : FunctionDeclaration 27 | 28 | /// The implementation of the function as a set of ``Statement``s. 29 | public var statements : [ Statement ] 30 | 31 | /// Initialize a new function definition AST node. 32 | public init(declaration : FunctionDeclaration, 33 | statements : [ Statement ], 34 | comment : FunctionComment? = nil, 35 | inlinable : Bool = false, discardableResult: Bool = false) 36 | { 37 | self.declaration = declaration 38 | self.statements = statements 39 | self.comment = comment 40 | self.inlinable = inlinable 41 | self.discardableResult = discardableResult 42 | } 43 | } 44 | 45 | 46 | // MARK: - Convenience 47 | 48 | public extension FunctionDefinition { 49 | 50 | /// Initialize a new function definition AST node. 51 | init(_ declaration : FunctionDeclaration, 52 | _ statements : Statement...) 53 | { 54 | self.init(declaration: declaration, statements: statements) 55 | } 56 | } 57 | 58 | #if swift(>=5.5) 59 | extension FunctionDefinition: Sendable {} 60 | #endif 61 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/FunctionParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | public extension FunctionDeclaration { 7 | 8 | /// A parameter, like `from table: KeyPath` 9 | /// Or: `limit: Int? = nil` 10 | struct Parameter: Equatable { 11 | 12 | /// The "label" of the parameter, e.g. the `from` in `select(from table:)`. 13 | /// Can be `nil` for a wildcard (`_`). 14 | public let keyword : String? // `from` 15 | /// The name of the parameter, not to be confused with the ``keyword``. 16 | public let name : String // `table` 17 | /// The type of the parameter, e.g. `.integer`. 18 | public let type : TypeReference // e.g. `KeyPath<...>` or `Int?` 19 | /// If set, the default value of the parameter (like in `limit: Int = 10`). 20 | public let defaultValue : Expression? // e.g. `nil` 21 | 22 | /// Initialize a new parameter for a function declaration. 23 | public init(keyword: String? = nil, name: String, type: TypeReference, 24 | defaultValue: Expression? = nil) 25 | { 26 | self.keyword = keyword 27 | self.name = name 28 | self.type = type 29 | self.defaultValue = defaultValue 30 | } 31 | } 32 | } 33 | 34 | 35 | // MARK: - Convenience 36 | 37 | public extension FunctionDeclaration.Parameter { 38 | 39 | /// Initialize a new parameter for a function declaration where the 40 | /// label is the same like the parameter name, e.g. `func x(a: Int)`. 41 | init(keywordArg name: String, _ type: TypeReference, 42 | _ defaultValue: Literal? = nil) 43 | { 44 | self.init(keyword: name, name: name, type: type, 45 | defaultValue: defaultValue.flatMap { .literal($0) }) 46 | } 47 | 48 | /// Initialize a new parameter for a function declaration. 49 | init(keyword: String? = nil, name: String, 50 | keyPath fromBase : String, _ fromBaseName : String, 51 | to toBase : String, _ toBaseName : String) 52 | { 53 | self.init( 54 | keyword: keyword, name: name, 55 | type: .keyPath( 56 | fromType : .qualifiedType(baseName: fromBase, name: fromBaseName), 57 | toType : .qualifiedType(baseName: toBase, name: toBaseName) 58 | ) 59 | ) 60 | } 61 | 62 | /// Initialize a new parameter for a function declaration. 63 | init(keyword: String? = nil, name: String, 64 | keyPath fromBase: String, _ fromBaseName: String, 65 | to toBaseName: String) 66 | { 67 | self.init( 68 | keyword: keyword, name: name, 69 | type: .keyPath( 70 | fromType : .qualifiedType(baseName: fromBase, name: fromBaseName), 71 | toType : .name(toBaseName) 72 | ) 73 | ) 74 | } 75 | 76 | /// Initialize a new parameter for a function declaration. 77 | init(keyword: String? = nil, name: String, 78 | closureParameters: [ ( base: String, name: String ) ], 79 | `throws`: Bool = false, 80 | returns: TypeReference = .void) 81 | { 82 | self.init( 83 | keyword: keyword, name: name, 84 | type: .closure( 85 | escaping: false, 86 | parameters: closureParameters.map { 87 | ( base, name ) in .qualifiedType(baseName: base, name: name) 88 | }, 89 | throws: `throws`, 90 | returns: returns 91 | ) 92 | ) 93 | } 94 | } 95 | 96 | #if swift(>=5.5) 97 | extension FunctionDeclaration.Parameter: Sendable {} 98 | #endif 99 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/GenericConstraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A generic constraint attached to a ``FunctionDeclaration`` generic parameter 8 | * or an ``Extension``. 9 | */ 10 | public enum GenericConstraint: Equatable { 11 | 12 | /// `C1: SQLColumn` 13 | case conformance(name: String, type: TypeReference) 14 | 15 | /// `T == C1.T` 16 | case equal(name: String, type: TypeReference) 17 | } 18 | 19 | 20 | // MARK: - Convenience 21 | 22 | public extension GenericConstraint { 23 | 24 | /// `C1: SQLColumn` 25 | static func conformance(_ name: String, to typeName: String) -> Self { 26 | .conformance(name: name, type: .name(typeName)) 27 | } 28 | 29 | /// `T == C1.T` 30 | static func parameter(_ name: String, 31 | sameAs typeNameBase: String, _ typeName: String) -> Self 32 | { 33 | .equal(name: name, 34 | type: .qualifiedType(baseName: typeNameBase, name: typeName)) 35 | } 36 | } 37 | 38 | #if swift(>=5.5) 39 | extension GenericConstraint: Sendable {} 40 | #endif 41 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/Literal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A Swift literal 8 | * 9 | * E.g. as used in default values like: `limit: Int? = nil` (the `nil`). 10 | */ 11 | public enum Literal: Equatable { 12 | 13 | /// `nil` 14 | case `nil` 15 | /// Bool `true` 16 | case `true` 17 | /// Bool `false` 18 | case `false` 19 | 20 | /// An integer literal, like `42` 21 | case integer(Int) 22 | 23 | /// A double literal, like `42.1337` 24 | case double(Double) 25 | 26 | /// A string literal, like `"Hello World"` 27 | case string(String) 28 | 29 | /// A literal array, like `[ 1973, 1, 31 ]` 30 | case integerArray([ Int ]) 31 | } 32 | 33 | #if swift(>=5.5) 34 | extension Literal: Sendable {} 35 | #endif 36 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/TypeComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /// A comment on top of e.g. a class or struct. 7 | public struct TypeComment: Equatable { 8 | 9 | /// One example in the type comment. There can be multiple. 10 | public struct Example: Equatable { 11 | 12 | /// A title for the example. 13 | public var headline : String 14 | /// The code for the example, will be emitted in triple backticks. 15 | public var code : String 16 | /// The programming language of the sample, e.g. `swift`. 17 | public var language : String? 18 | 19 | /// Initialize a new type example. 20 | public init(headline: String, code: String, language: String? = "swift") { 21 | self.headline = headline 22 | self.code = code.replacingOccurrences(of: "\r\n", with: "\n") 23 | self.language = language 24 | } 25 | } 26 | 27 | /// The title of the comment for the type/structure. 28 | public var headline : String 29 | 30 | /// If set, a larger paragraph describing the type. 31 | public var info : String? 32 | 33 | /// A set of examples on how to use the structure. 34 | public var examples : [ Example ] 35 | 36 | /// Special thing, SQL related to the structure. 37 | public var sql : [ Example ] 38 | 39 | /// Initialize a new comment for a type. 40 | public init(headline : String, info: String? = nil, 41 | examples : [ Example ] = [], 42 | sql : [ Example ] = []) 43 | { 44 | self.headline = headline 45 | self.info = info 46 | self.examples = examples 47 | self.sql = sql 48 | } 49 | } 50 | 51 | #if swift(>=5.5) 52 | extension TypeComment : Sendable {} 53 | extension TypeComment.Example : Sendable {} 54 | #endif 55 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/Nodes/TypeReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A reference to some type, e.g. `Void` or `Person` or `Int`. 8 | */ 9 | public indirect enum TypeReference: Equatable { 10 | 11 | /// `Void` 12 | case void 13 | 14 | /// Simple type name, like `SQLColumn` 15 | case name(String) 16 | 17 | /// `some SQLColumn` 18 | case some(String) 19 | 20 | /// `Self.RecordTypes` or `T.Type` 21 | case qualifiedType(baseName: String, name: String) 22 | 23 | /// `KeyPath` 24 | case keyPath(fromType: TypeReference, toType: TypeReference) 25 | 26 | /// ( C1.Value, C2.Value ) -> Void 27 | case closure(escaping: Bool, parameters: [ TypeReference ], 28 | `throws`: Bool, returns: TypeReference) 29 | 30 | /// ( column1: C1.Value, column2: C2.Value ) 31 | case tuple(names: [ String? ], types: [ TypeReference ]) 32 | 33 | /// [ ( column1: C1.Value, column2: C2.Value ) ] 34 | case array(TypeReference) 35 | 36 | /// `Int?` 37 | case optional(TypeReference) 38 | 39 | /// `inout Person` 40 | case `inout`(TypeReference) 41 | } 42 | 43 | public extension TypeReference { 44 | 45 | 46 | /// Swift `Any`. 47 | static var any : TypeReference { .name("Any") } 48 | 49 | /// Swift `Int`. 50 | static let int = TypeReference.name("Int") 51 | /// Swift `String`. 52 | static let string = TypeReference.name("String") 53 | /// Swift `Double`. 54 | static let double = TypeReference.name("Double") 55 | /// Swift `Bool`. 56 | static let bool = TypeReference.name("Bool") 57 | 58 | // common in SQLite 59 | /// Swift `Int32`. 60 | static let int32 = TypeReference.name("Int32") 61 | /// Swift `Int64`. 62 | static let int64 = TypeReference.name("Int64") 63 | } 64 | 65 | public extension TypeReference { 66 | /// Swift `[ UInt8 ]`. Aka `Data`, w/o the need for Foundation. 67 | static let uint8Array = TypeReference.array(.name("UInt8")) 68 | } 69 | 70 | public extension TypeReference { 71 | 72 | /// A Foundation `URL`. 73 | static let url = TypeReference.name("URL") 74 | /// A Foundation `Decimal` number. 75 | static let decimal = TypeReference.name("Decimal") 76 | /// A Foundation `Date`. 77 | static let date = TypeReference.name("Date") 78 | /// A Foundation `Data`. 79 | static let data = TypeReference.name("Data") 80 | /// A Foundation `UUID`. 81 | static let uuid = TypeReference .name("UUID") 82 | } 83 | 84 | #if swift(>=5.5) 85 | extension TypeReference : Sendable {} 86 | #endif 87 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterCodeGenAST/README.md: -------------------------------------------------------------------------------- 1 |

Lighter Code Generation AST 2 | 4 |

5 | 6 | A very simplistic AST for code generation purposes within Enlighter. 7 | 8 | This is not supposed to represent full Swift at all, just what is used 9 | within the code generation plugins. 10 | 11 | Pretty hacky, close your eyes when looking at this particular code. 12 | 13 | 14 | ### Who 15 | 16 | Lighter is brought to you by 17 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 18 | We like feedback, GitHub stars, cool contract work, 19 | presumably any form of praise you can think of. 20 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/CodeGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import LighterCodeGenAST 7 | import struct Foundation.URL 8 | import struct Foundation.Date 9 | import struct Foundation.Data 10 | import class Foundation.ISO8601DateFormatter 11 | 12 | public extension CompilationUnit { 13 | 14 | func writeCode(to url: URL, configuration config: LighterConfiguration, 15 | headerName: String? = nil, now: Date = Date()) throws 16 | { 17 | let formatter = ISO8601DateFormatter() 18 | 19 | let codegen = CodeGenerator(configuration: config.codeStyle) 20 | 21 | if let toolName = headerName { 22 | codegen.writeln( 23 | "// Autocreated by \(toolName) at \(formatter.string(from: now))") 24 | } 25 | 26 | if !imports.isEmpty && reexports.isEmpty { codegen.writeln() } 27 | 28 | codegen.generateUnit(self) 29 | 30 | let data = Data(codegen.source.utf8) 31 | try data.write(to: url, options: [ .atomic ]) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/GenModel/DatabaseInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * The model object describing a SQLite database mapped to Swift. 8 | * 9 | * Has a name, like "Contacts", and a set of ``EntityInfo``s for each table and 10 | * view. 11 | * Plus the "userVersion" set in SQLite, which can be useful to implement 12 | * migrations and detect whether the generated code matches an actual database 13 | * schema. 14 | */ 15 | public final class DatabaseInfo { 16 | 17 | /// The name of the database structure, e.g. `Contacts` 18 | public var name : String 19 | /// The user settable schema version of the database. 20 | public let userVersion : Int 21 | /// The set of tables and views in the database. 22 | public let entities : [ EntityInfo ] 23 | 24 | /// Initialize a new database model object. 25 | public init(name: String, userVersion: Int = 0, entities: [ EntityInfo ] = []) 26 | { 27 | self.name = name 28 | self.userVersion = userVersion 29 | self.entities = entities 30 | } 31 | 32 | /// Lookup an ``EntityInfo`` by its "Swift name" (e.g. "Person"). 33 | public subscript(_ name: String) -> EntityInfo? { 34 | entities.first(where: { $0.name == name }) 35 | } 36 | /// Lookup an ``EntityInfo`` by its "SQL name" (e.g. "person"). 37 | public subscript(externalName name: String) -> EntityInfo? { 38 | entities.first(where: { $0.externalName == name }) 39 | } 40 | 41 | /// The set of the (Swift) names of all tables and views. 42 | public var entityNames : Set { Set(entities.map(\.name)) } 43 | } 44 | 45 | extension DatabaseInfo: CustomStringConvertible { 46 | 47 | /// Returns a description of the database information. 48 | public var description: String { 49 | var ms = " Destination? { 23 | * try self[dynamicMember: \.sourceReferenceName] 24 | * .findTarget(for: \.sourceProperty, in: sourceRecord) 25 | * } 26 | * ``` 27 | */ 28 | struct ToOne { // attached to source entity 29 | 30 | public let name : String // Person (=> findPerson) 31 | public let destinationEntity : EntityInfo // Person 32 | public let sourcePropertyName : String // personId (=> \.personId) 33 | public let isPrimary : Bool // whether name must be added 34 | 35 | public var hasNameMatchingDestinationRecord : Bool { 36 | name == destinationEntity.name 37 | } 38 | } 39 | 40 | /** 41 | * Relationship for the inverse of the foreign-key. 42 | * 43 | * Technically this could still be a 1:1, but we can't know just from the 44 | * schema and hence must assume 1:n. 45 | * 46 | * Note: This should probably not have multiple matches. 47 | * 48 | * Will generate on `SQLDatabaseFetchOperations` for name-matching ones: 49 | * ``` 50 | * func fetchSource[es](for destinationRecord: Destination, 51 | * omitEmpty: Bool = false, limit: Int? = nil) 52 | * throws -> [ Source ] 53 | * { 54 | * try self[dynamicMember: \.sourceReferenceName] 55 | * .fetch(for: \.sourceProperty, in: destinationRecord, limit: limit) 56 | * } 57 | * ``` 58 | */ 59 | struct ToMany { // attached to destination entity 60 | 61 | public let name : String // Addresses (=> fetchAddresses) 62 | public let sourceEntity : EntityInfo // Address 63 | public let sourcePropertyName : String // personId (=> \.personId) 64 | public let qualifierParameter : String? // `Owner` (=> `forOwner:`) 65 | } 66 | } 67 | 68 | extension EntityInfo.ToOne: CustomStringConvertible { 69 | 70 | public var description: String { 71 | var ms = " CommentStyle { 15 | if let s = CommentStyle(rawValue: s) { return s } 16 | if s == "none" || s == "no" || s == "false" { return .noComments } 17 | print("Unexpected comment style:", s) 18 | return .doubleStars 19 | } 20 | 21 | if let section = section[section: "comments"] { 22 | if let v = section[string: "types"] { 23 | self.typeCommentStyle = commentStyle(v) 24 | } 25 | if let v = section[string: "properties"] { 26 | self.propertyCommentStyle = commentStyle(v) 27 | } 28 | if let v = section[string: "functions"] { 29 | self.functionCommentStyle = commentStyle(v) 30 | } 31 | } 32 | else if let s = section[string: "comments"] { 33 | let v = commentStyle(s) 34 | self.typeCommentStyle = v 35 | self.propertyCommentStyle = v 36 | self.functionCommentStyle = v 37 | } 38 | 39 | if let v = section["indent"] { 40 | if let v = v as? Int { self.indent = String(repeating: " ", count: v) } 41 | else if let v = v as? String { self.indent = v } 42 | } 43 | 44 | if let v = section[int: "lineLength"] { self.lineLength = v } 45 | if let v = section[bool: "neverInline"] { self.neverInline = v } 46 | 47 | if let v = section[string: "endOfLineString"] { self.eol = v } 48 | if let v = section[string: "identifierListSeparator"] { 49 | self.identifierListSeparator = v 50 | } 51 | if let v = section[string: "typeConformanceSeparator"] { 52 | self.typeConformanceSeparator = v 53 | } 54 | if let v = section[string: "propertyTypeSeparator"] { 55 | self.propertyTypeSeparator = v 56 | } 57 | if let v = section[string: "propertyValueSeparator"] { 58 | self.propertyValueSeparator = v 59 | } 60 | if let v = section[string: "multiExampleSectionHeader"] { 61 | self.multiExampleSectionHeader = v 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/LighterConfiguration/FancyfierConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | public extension Fancifier.Options { 7 | 8 | // SwiftMapping dictionary 9 | init(section: ConfigFile.Section?) { 10 | self.init() 11 | guard let section = section else { return } 12 | 13 | if let section = section[section: "keys"] { 14 | if let v = section["autodetect"] as? [ String ] { 15 | autodetectKeyNames = v 16 | } 17 | if let v = section[bool: "autodetectWithTableName"] { 18 | autodetectPrimaryKeyNamesWithTable = v 19 | } 20 | if let v = section[bool: "autodetectForeignKeys"] { 21 | autodetectForeignKeys = v 22 | } 23 | if let v = section[bool: "autodetectForeignKeysInViews"] { 24 | autodetectForeignKeysInViews = v 25 | } 26 | if let v = section[string: "primaryKeyName"] { forcePrimaryKeyName = v } 27 | } 28 | 29 | if let name = section[string: "databaseTypeName"], !name.isEmpty { 30 | forceDatabaseName = name 31 | } 32 | else if let section = section[section: "databaseTypeName"] { 33 | if let v = section[bool: "dropFileExtensions"] { 34 | dropDatabaseFileExtensions = v 35 | } 36 | if let v = section[bool: "capitalize"] { capitalizeDatabaseName = v } 37 | if let v = section[bool: "camelCase"] { camelCaseDatabaseName = v } 38 | } 39 | 40 | if let section = section[section: "recordTypeNames"] { 41 | // false by default 42 | if let v = section[bool: "singularize"] { singularizeRecordNames = v } 43 | if let v = section[bool: "capitalize"] { capitalizeRecordNames = v } 44 | if let v = section[bool: "camelCase"] { camelCaseRecordNames = v } 45 | } 46 | if let section = section[section: "recordReferenceNames"] { 47 | if let v = section[bool: "decapitalize"] { 48 | decapitalizeRecordReferenceName = v 49 | } 50 | if let v = section[bool: "pluralize"] { 51 | pluralizeRecordReferenceName = v 52 | } 53 | } 54 | 55 | if let section = section[section: "propertyNames"] { 56 | if let v = section[bool: "decapitalize"] { 57 | decapitalizePropertyNames = v 58 | } 59 | if let v = section[bool: "camelCase"] { camelCasePropertyNames = v } 60 | } 61 | 62 | if let section = section[section: "CoreData"] { 63 | if let v = section[bool: "removeZAndCapitalizeRecordNames"] { 64 | capitalizeAndDeZAllUpperRecordNames = v 65 | } 66 | if let v = section[bool: "removeZAndLowercasePropertyNames"] { 67 | lowercaseAndDeZAllUpperZPropertyNames = v 68 | } 69 | } 70 | if let section = section[section: "Swift"] { 71 | if let s = section[string: "spaceReplacementStringForIDs"] { 72 | spaceReplacementStringForIDs = s 73 | } 74 | } 75 | 76 | if let section = section[section: "relationships"] { 77 | if let v = section[bool: "deriveFromForeignKeys"] { 78 | deriveRelationshipsFromForeignKeys = v 79 | } 80 | if let v = section["strippedForeignKeySuffixes"] as? [ String ] { 81 | relationshipForeignKeyStripSuffix = v 82 | } 83 | } 84 | 85 | func propertyType(for string: String) -> EntityInfo.Property.PropertyType { 86 | switch string { 87 | case "integer" : return .integer 88 | case "double" : return .double 89 | case "string" : return .string 90 | case "uint8Array" : return .uint8Array 91 | case "bool" : return .bool 92 | case "date" : return .date 93 | case "data" : return .data 94 | case "url" : return .url 95 | case "decimal" : return .decimal 96 | case "uuid" : return .uuid 97 | default : return .custom(string) 98 | } 99 | } 100 | 101 | if let v = section["typeMap"] as? [ String: String ] { 102 | sqlTypeToSwiftType = v.mapValues(propertyType(for:)) 103 | } 104 | if let v = section["columnSuffixToType"] as? [ String: String ] { 105 | columnSuffixToSwiftType = v.mapValues(propertyType(for:)) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/LighterConfiguration/JSONUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /// Just a `[ String : Any ]`, as common in JSON. 7 | public typealias JSONDict = [ String : Any ] 8 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/LighterConfiguration/LighterConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | import LighterCodeGenAST 7 | 8 | /** 9 | * This represents the contents of the `Lighter.json` file used to configure 10 | * the Enlighter SPM plugin tools. 11 | * 12 | * Example: 13 | * ```json 14 | * "CodeStyle": { 15 | * "functionCommentStyle" : "**", 16 | * "indent" : " ", 17 | * "lineLength" : 80 18 | * }, 19 | * 20 | * "EmbeddedLighter": { 21 | * "selects": { 22 | * "syncYield" : { "columns": 8, "sorts": 2 }, 23 | * "syncArray" : { "columns": 8, "sorts": 2 }, 24 | * "asyncArray" : { "columns": 8, "sorts": 2 } 25 | * } 26 | * }, 27 | * 28 | * "SwiftMapping": { 29 | * "databaseTypeName": { ... } 30 | * } 31 | * ``` 32 | */ 33 | public struct LighterConfiguration: Equatable, Sendable { 34 | 35 | public static let filename = "Lighter.json" 36 | 37 | public var codeStyle : CodeGenerator.Configuration 38 | public var embeddedLighter : EmbeddedLighter 39 | public var swiftMapping : Fancifier.Options 40 | public var codeGeneration : EnlighterASTGenerator.Options 41 | 42 | public var isEmpty : Bool { false } 43 | 44 | public init(codeStyle : CodeGenerator.Configuration, 45 | embeddedLighter : EmbeddedLighter, 46 | swiftMapping : Fancifier.Options, 47 | codeGeneration : EnlighterASTGenerator.Options) 48 | { 49 | self.codeStyle = codeStyle 50 | self.embeddedLighter = embeddedLighter 51 | self.swiftMapping = swiftMapping 52 | self.codeGeneration = codeGeneration 53 | } 54 | 55 | public static let `default` = 56 | LighterConfiguration( 57 | codeStyle: .init(), embeddedLighter: .init(), swiftMapping: .init(), 58 | codeGeneration: .init() 59 | ) 60 | } 61 | 62 | import struct Foundation.Data 63 | import struct Foundation.URL 64 | 65 | public extension LighterConfiguration { 66 | 67 | init(file: ConfigFile) { 68 | self.init(section: file.root) 69 | } 70 | 71 | init(contentsOf url: URL, for target: String? = nil, stem: String? = nil) 72 | throws 73 | { 74 | self.init(file: try ConfigFile(contentsOf: url, for: target, stem: stem)) 75 | } 76 | init(data: Data, for target: String? = nil, stem: String? = nil) throws { 77 | self.init(file: try ConfigFile(data: data, for: target, stem: stem)) 78 | } 79 | init(json: JSONDict, for target: String? = nil, stem: String? = nil) { 80 | self.init(file: ConfigFile(json: json, for: target, stem: stem)) 81 | } 82 | } 83 | 84 | extension LighterConfiguration: CustomStringConvertible { 85 | 86 | public var description: String { 87 | var ms = "Lighter Configuration 2 | 4 | 5 | 6 | A litte weird set of structures to support the override based configuration 7 | system. 8 | Should be reworked. 9 | 10 | The basic idea is that the all the values can be nil to allow stacking. 11 | I.e. multiple `LighterConfiguration` objects can be merged together. 12 | 13 | 14 | ### Who 15 | 16 | Lighter is brought to you by 17 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 18 | We like feedback, GitHub stars, cool contract work, 19 | presumably any form of praise you can think of. 20 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/README.md: -------------------------------------------------------------------------------- 1 |

Lighter Code Generation 2 | 4 |

5 | 6 | This contains the code generation parts of Enlighter. 7 | 8 | Not a beautiful thing, but hey, it is a code generator! :-) 9 | 10 | Parts: 11 | 12 | - [RecordGeneration](RecordGeneration/) is the main code generator that produces 13 | Swift AST nodes from the [GenModel](GenModel/) as configured in the 14 | [LighterConfiguration](LighterConfiguration/). 15 | - [GenModel](GenModel/) represents the database model that should be generated. 16 | If is derived from a schema as loaded by the 17 | [SchemaLoader](SchemaLoader.swift). 18 | - [LighterConfiguration](LighterConfiguration/) deals with loading and 19 | processing the `Lighter.json` configuration file. 20 | - [VariadicGeneration](VariadicGeneration/) implements the generation of the 21 | variadic functions for column selects and updates (e.g. `select(...), 22 | `select(...)` etc). 23 | 24 | 25 | ### Who 26 | 27 | Lighter is brought to you by 28 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 29 | We like feedback, GitHub stars, cool contract work, 30 | presumably any form of praise you can think of. 31 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/RecordGeneration/GenerateCombinedFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import LighterCodeGenAST 7 | 8 | extension EnlighterASTGenerator { 9 | 10 | /** 11 | * Generates one big `CompilationUnit` containing all the code for the 12 | * various parts representing the database. Including the struct for the 13 | * database, for the records and all the various functions, global or not. 14 | */ 15 | public func generateCombinedFile(moduleFileName: String?) -> CompilationUnit { 16 | var unit = CompilationUnit(name: filename) 17 | 18 | unit.imports = [ "SQLite3" ] // TBD: re-export for raw mode? 19 | if options.allowFoundation || options.useLighter { 20 | // FIXME: Lighter uses `Foundation` for URL inits. 21 | unit.imports.append("Foundation") 22 | } 23 | if options.useLighter { 24 | switch options.importLighter { 25 | case .none : break 26 | case .import : unit.imports .append("Lighter") 27 | case .reexport : unit.reexports.append("Lighter") 28 | } 29 | } 30 | 31 | // Global raw functions 32 | 33 | if case .globalFunctions(let prefix) = options.rawFunctions { 34 | let lowerName = database.name.lowercased().snake_case() 35 | if shouldGenerateCreateSQL { 36 | let name = prefix + "create_" + lowerName 37 | unit.functions.append( 38 | generateRawCreateFunction(name: name, moduleFileName: moduleFileName) 39 | ) 40 | } 41 | if let filename = moduleFileName { 42 | let name = prefix + "open_" + lowerName 43 | unit.functions.append( 44 | generateRawModuleOpenFunction(name: name, for: filename)) 45 | } 46 | for entity in database.entities { 47 | unit.functions += generateRawFunctions(for: entity) 48 | } 49 | } 50 | 51 | 52 | // Database Structure 53 | 54 | unit.typeDefinitions.append( 55 | generateDatabaseStructure(moduleFileName: moduleFileName)) 56 | 57 | 58 | // Entity Structures 59 | 60 | if !options.nestRecordTypesInDatabase { 61 | unit.typeDefinitions += database.entities.map { 62 | generateRecordStructure(for: $0) 63 | } 64 | } 65 | 66 | // Type-Attached Raw Functions in Extensions (`Person.update(in:)` etc) 67 | 68 | if options.rawFunctions == .attachToRecordType { 69 | unit.extensions += database.entities.map { entity in 70 | Extension(extendedType: globalTypeRef(of: entity), 71 | typeFunctions: generateRawTypeFunctions(for: entity), 72 | functions: generateRawFunctions(for: entity)) 73 | } 74 | } 75 | 76 | // Schema Structures, schema inits, binds and matching 77 | 78 | unit.extensions += database.entities.map { entity in 79 | Extension( 80 | extendedType: globalTypeRef(of: entity), 81 | typeDefinitions: [ generateSchemaStructure(for: entity) ], 82 | functions: [ 83 | generateRecordStatementInit(for: entity), 84 | generateRecordStatementBind(for: entity) 85 | ] 86 | ) 87 | } 88 | 89 | // Relationships 90 | 91 | if options.useLighter, options.generateLighterRelationships { 92 | unit.extensions += generateRecordRelshipExtensions() 93 | if options.asyncAwait { 94 | unit.extensions += generateRecordRelshipExtensions(async: true) 95 | } 96 | } 97 | 98 | return unit 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/RecordGeneration/GeneratePropertyType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import LighterCodeGenAST 7 | 8 | extension EnlighterASTGenerator { 9 | 10 | func type(for property: EntityInfo.Property) -> TypeReference { 11 | property.isNotNull 12 | ? baseType(for: property) 13 | : .optional(baseType(for: property)) 14 | } 15 | func baseType(for property: EntityInfo.Property) -> TypeReference { 16 | if options.allowFoundation { 17 | switch property.propertyType { 18 | case .integer : return .int 19 | case .double : return .double 20 | case .string : return .string 21 | case .uint8Array : return .uint8Array 22 | case .bool : return .bool 23 | case .date : return .date 24 | case .data : return .data 25 | case .url : return .url 26 | case .decimal : return .decimal 27 | case .uuid : return .uuid 28 | case .custom(let type) : return .name(type) 29 | } 30 | } 31 | else { // Not Foundation Types are allowed! 32 | switch property.propertyType { 33 | case .integer : return .int 34 | case .double : return .double 35 | case .string : return .string 36 | case .uint8Array : return .uint8Array 37 | case .bool : return .bool 38 | case .date : return options.dateStorageStyle == .formatter 39 | ? .string : .int 40 | case .data : return .uint8Array 41 | case .url : return .string 42 | case .decimal : return .string 43 | case .uuid : return .string 44 | case .custom(let type) : return .name(type) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/Utilities/ConcurrencyCompat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | 7 | #if !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && swift(>=5.9) 8 | #if compiler(<6) // seems necessary? 9 | import struct Foundation.CharacterSet 10 | import class Foundation.DateFormatter 11 | extension CharacterSet : @unchecked Sendable {} 12 | extension DateFormatter : @unchecked Sendable {} 13 | #endif 14 | #endif 15 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/Utilities/SQLGeneration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2023 ZeeZide GmbH. 4 | // 5 | 6 | #if canImport(Foundation) 7 | import Foundation // for replacingOccurrencesOf 8 | 9 | /// Escape the `id` (e.g. a column or table name) and surround it by quotes. 10 | @inlinable 11 | func escapeAndQuoteIdentifier(_ id: String) -> String { 12 | id.contains("\"") 13 | ? "\"\(id.replacingOccurrences(of: "\"", with: "\"\""))\"" 14 | : "\"\(id)\"" 15 | } 16 | #else // no Foundation 17 | 18 | /// Escape the `id` (e.g. a column or table name) and surround it by quotes. 19 | @inlinable 20 | func escapeAndQuoteIdentifier(_ id: String) -> String { 21 | id.firstIndex(of: "\"") != nil 22 | ? "\"\(id.split(whereSeparator: { $0 == "\""}).joined(separator: "\"\""))\"" 23 | : "\"\(id)\"" 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/VariadicGeneration/FunctionGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import struct Foundation.Locale 7 | import class Foundation.NumberFormatter 8 | import LighterCodeGenAST 9 | 10 | /** 11 | * Common base class for Lighter variadic function generators. 12 | */ 13 | public class FunctionGenerator { 14 | 15 | public var columnCount : Int = 6 { 16 | didSet { assert(columnCount >= 1 && columnCount < 32) } 17 | } 18 | 19 | public var api = LighterAPI() 20 | 21 | public var recordGenericParameterPrefix = "T" 22 | public var predicateGenericParameterPrefix = "P" 23 | public var columnGenericParameterPrefix = "C" 24 | public var columnParameterName = "column" 25 | 26 | public var builderVariableName = "builder" 27 | 28 | // Note: Those should be filled based on the schema, if available!! 29 | // I.e. take an actual table + columns from the schema. 30 | // Though that affects the qualifier? 31 | public var commentRecordExample = "person" 32 | public var commentColumnValueExamples : [ (column: String, value: String)] = [ 33 | ( "personId" , "10" ), 34 | ( "lastname" , "\"Duck\"" ), 35 | ( "city" , "\"Entenhausen\"" ), 36 | ( "street" , "\"Am Geldspeicher 1\"" ), 37 | ( "leetness" , "1337" ) 38 | ] 39 | public lazy var commentColumnExamples = 40 | commentColumnValueExamples.map { $0.column } 41 | 42 | /* Result */ 43 | public var functions = [ FunctionDefinition ]() 44 | 45 | 46 | /** 47 | * Returns names with a certain prefix: 48 | * - count 0: `[]` 49 | * - count 1: `[ prefix ]` 50 | * - count 2+: `[ prefix1, prefix2, ... ]` 51 | */ 52 | func oneBasedNames(prefix: String, count: Int) -> [ String ] { 53 | guard count > 0 else { return [] } 54 | if count == 1 { return [ `prefix` ] } 55 | return (1...count).map { "\(prefix)\($0)" } 56 | } 57 | 58 | /** 59 | * Format numbers into English ordinal numbers (1=>1st, 2=>2nd) 60 | */ 61 | private let ordinalFormatter : NumberFormatter = { 62 | let fmt = NumberFormatter() 63 | fmt.numberStyle = .ordinal 64 | fmt.locale = Locale(identifier: "en_US") 65 | return fmt 66 | }() 67 | func ordinal(_ value: Int) -> String { 68 | ordinalFormatter.string(for: value) ?? String(value) 69 | } 70 | 71 | public init() {} 72 | } 73 | -------------------------------------------------------------------------------- /Plugins/Libraries/LighterGeneration/VariadicGeneration/GenerateInternalVariadics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | import LighterCodeGenAST 8 | 9 | /// Generates a `CompilationUnit` with the column `select`, `update` and 10 | /// `insert` functions with various numbers of arguments. 11 | public func generateInternalVariadics( 12 | filename: String, 13 | configuration config: LighterConfiguration 14 | ) 15 | -> CompilationUnit 16 | { 17 | var fetchOps = Extension(extendedType: .name("SQLDatabaseFetchOperations")) 18 | var changeOps = Extension(extendedType: .name("SQLDatabaseChangeOperations")) 19 | var asyncFetchOps = 20 | Extension(extendedType: .name("SQLDatabaseAsyncFetchOperations")) 21 | asyncFetchOps.minimumSwiftVersion = ( 5, 5 ) 22 | asyncFetchOps.requiredImports = [ "_Concurrency" ] 23 | 24 | // MARK: - Selects 25 | 26 | if !config.embeddedLighter.selects.syncYield.isDisabled { 27 | let sel = config.embeddedLighter.selects.syncYield 28 | let generator = SelectFunctionGeneration( 29 | columnCount : sel.columns, 30 | sortCount : sel.sorts, 31 | yield: true, async: false 32 | ) 33 | generator.predicateParameterName = nil // crashes swiftc 34 | generator.generate() 35 | fetchOps.functions += generator.functions 36 | } 37 | if !config.embeddedLighter.selects.syncArray.isDisabled { 38 | let sel = config.embeddedLighter.selects.syncArray 39 | let generator = SelectFunctionGeneration( 40 | columnCount : sel.columns, 41 | sortCount : sel.sorts, 42 | yield: false, async: false 43 | ) 44 | generator.generate() 45 | generator.predicateParameterName = nil // also need the one w/o the predicate 46 | generator.generate() 47 | 48 | fetchOps.functions += generator.functions 49 | } 50 | if !config.embeddedLighter.selects.asyncArray.isDisabled { 51 | let sel = config.embeddedLighter.selects.asyncArray 52 | let generator = SelectFunctionGeneration( 53 | columnCount : sel.columns, 54 | sortCount : sel.sorts, 55 | yield: false, async: true 56 | ) 57 | generator.generate() 58 | generator.predicateParameterName = nil // also need the one w/o the predicate 59 | generator.generate() 60 | 61 | asyncFetchOps.functions += generator.functions 62 | } 63 | 64 | 65 | // MARK: - Updates 66 | 67 | if config.embeddedLighter.updates.keyBased > 0 { 68 | let up = config.embeddedLighter.updates.keyBased 69 | let generator = UpdateFunctionGeneration( 70 | columnCount: up, primaryKey: true, async: false 71 | ) 72 | generator.generate() 73 | changeOps.functions += generator.functions 74 | } 75 | if config.embeddedLighter.updates.predicateBased > 0 { 76 | let up = config.embeddedLighter.updates.predicateBased 77 | let generator = UpdateFunctionGeneration( 78 | columnCount: up, primaryKey: false, async: false 79 | ) 80 | generator.generate() 81 | changeOps.functions += generator.functions 82 | } 83 | 84 | 85 | // MARK: - Inserts 86 | 87 | if config.embeddedLighter.inserts > 0 { 88 | let generator = InsertFunctionGeneration( 89 | columnCount: config.embeddedLighter.inserts, async: false) 90 | generator.generate() 91 | changeOps.functions += generator.functions 92 | } 93 | 94 | 95 | // Forge a Unit 96 | 97 | var unit = CompilationUnit(name: filename) 98 | unit.addIfNotEmpty(fetchOps) 99 | unit.addIfNotEmpty(asyncFetchOps) 100 | unit.addIfNotEmpty(changeOps) 101 | 102 | return unit 103 | } 104 | -------------------------------------------------------------------------------- /Plugins/Tools/GenerateInternalVariadics/README.md: -------------------------------------------------------------------------------- 1 |

Tool: GenerateInternalVariadics 2 | 4 |

5 | 6 | This is a tool that is used as part of this package only (i.e. it is not 7 | a product). 8 | 9 | It is called by the `WriteInternalVariadics` command plugin to generate the 10 | "Lighter/Operations/GeneratedVariadicOperations.swift" file. 11 | 12 | Which contains the variadic functions for `select`, `update` etc, e.g.: 13 | ```swift 14 | @inlinable 15 | func select( 16 | from tableOrView: KeyPath, 17 | _ column1: KeyPath, 18 | _ column2: KeyPath, 19 | orderBy sortColumn: KeyPath, 20 | _ direction: SQLSortOrder = .ascending, 21 | _ limit: Int? = nil 22 | ) async throws -> [ ( column1: C1.Value, column2: C2.Value ) ] 23 | where C1: SQLColumn, C2: SQLColumn, CS: SQLColumn, T == C1.T, T == C2.T, T == CS.T 24 | ``` 25 | 26 | 27 | ### Who 28 | 29 | Lighter is brought to you by 30 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 31 | We like feedback, GitHub stars, cool contract work, 32 | presumably any form of praise you can think of. 33 | -------------------------------------------------------------------------------- /Plugins/Tools/GenerateInternalVariadics/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | import LighterGeneration 8 | import LighterCodeGenAST 9 | 10 | fileprivate enum ExitCodes: Int32 { 11 | case invalidArguments = 1 12 | case couldNotOpenConfiguration = 2 13 | case couldNotWriteOutput = 3 14 | } 15 | 16 | 17 | // MARK: - Process Arguments 18 | 19 | fileprivate let args = CommandLine.arguments 20 | fileprivate let toolName = URL(fileURLWithPath: args.first ?? "tool").lastPathComponent 21 | 22 | guard args.count > 3 else { 23 | print("Usage: \(toolName) ") 24 | exit(ExitCodes.invalidArguments.rawValue) 25 | } 26 | 27 | fileprivate let configURL = URL(fileURLWithPath: args[1]) 28 | fileprivate let targetName = args[2] 29 | fileprivate let outputURL = URL(fileURLWithPath: args[3]) 30 | 31 | 32 | // MARK: - Load Configuration 33 | 34 | fileprivate let config : LighterConfiguration 35 | do { 36 | config = try LighterConfiguration(contentsOf: configURL, for: targetName) 37 | } 38 | catch { 39 | print("Failed to load config from:\n ", configURL.path, "\n error:", error) 40 | exit(ExitCodes.couldNotOpenConfiguration.rawValue) 41 | } 42 | 43 | 44 | // MARK: - Generate the AST 45 | 46 | fileprivate let unit = generateInternalVariadics( 47 | filename: outputURL.lastPathComponent, 48 | configuration: config 49 | ) 50 | 51 | 52 | // MARK: - Write the AST 53 | 54 | do { 55 | try unit.writeCode(to: outputURL, configuration: config, headerName: toolName) 56 | } 57 | catch { 58 | print("Failed to write to", outputURL.path, "error:", error) 59 | exit(ExitCodes.couldNotWriteOutput.rawValue) 60 | } 61 | -------------------------------------------------------------------------------- /Plugins/Tools/sqlite2swift/Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import struct Foundation.URL 7 | import func Foundation.exit 8 | 9 | /** 10 | * Arguments: 11 | * - path to Lighter.json config file 12 | * - name of target, for Lighter.json processing 13 | * - array of input files, either SQL source or SQLite databases 14 | * - the path to the output file 15 | */ 16 | struct Arguments { 17 | 18 | /// Name of the tool, `sqlite2swift` 19 | let toolName : String 20 | /// Enable verbose logging 21 | let verbose : Bool 22 | /// Whether one of the input files is also a package resource. 23 | let moduleFilename : String? 24 | 25 | /// The URL to the configuration file, Lighter.json. 26 | let configURL : URL 27 | /// The name of the Swift package target, used for resolving configuration. 28 | let targetName : String // used for config resolution 29 | /// The input files to be processed. 30 | let inputURLs : [ URL ] 31 | /// The single output file to emit the code into. 32 | let outputURL : URL 33 | 34 | /// The first input filename w/o its extension. 35 | let stem : String 36 | 37 | /// Parse the arguments from an array of String's. 38 | init(arguments: [ String ] = CommandLine.arguments) throws { 39 | var args = arguments 40 | toolName = URL(fileURLWithPath: args.first ?? "tool").lastPathComponent 41 | 42 | let asksForHelp = args.contains("-h") || args.contains("--help") 43 | if asksForHelp || args.count < 5 { 44 | Self.usage(toolName) 45 | throw asksForHelp ? ExitCodes.ok : ExitCodes.invalidArguments 46 | } 47 | 48 | args.remove(at: args.startIndex) 49 | 50 | if let idx = (args.firstIndex(of: "--verbose") ?? args.firstIndex(of: "-v")) 51 | { 52 | verbose = true 53 | args.remove(at: idx) 54 | } 55 | else { 56 | verbose = false 57 | } 58 | 59 | var wantsModuleFilename = false 60 | var moduleFilename : String? 61 | if let idx = args.firstIndex(of: "--module-filename") { 62 | wantsModuleFilename = true 63 | let valueIdx = args.index(after: idx) 64 | if valueIdx < args.endIndex { 65 | moduleFilename = args[valueIdx] 66 | args.remove(at: valueIdx) 67 | } 68 | else { 69 | print("No module filename?!") 70 | } 71 | args.remove(at: idx) 72 | } 73 | 74 | if args.count < 4 { 75 | Self.usage(toolName) 76 | throw ExitCodes.invalidArguments 77 | } 78 | 79 | configURL = args[0] != "default" 80 | ? URL(fileURLWithPath: args[0]) 81 | : URL(string: "default:")! 82 | targetName = args[1] 83 | inputURLs = args.dropFirst(2).dropLast().map(URL.init(fileURLWithPath:)) 84 | outputURL = args.last.map(URL.init(fileURLWithPath:))! 85 | 86 | guard let firstFileName = inputURLs 87 | .first.flatMap({ $0.lastPathComponent }) else 88 | { 89 | exit(ExitCodes.invalidArguments.rawValue) 90 | } 91 | stem = firstFileName.firstIndex(where: { $0 == "-" || $0 == "." }) 92 | .flatMap { String(firstFileName[..<$0]) } 93 | ?? firstFileName 94 | 95 | if wantsModuleFilename { 96 | self.moduleFilename = moduleFilename ?? inputURLs.first?.lastPathComponent 97 | } 98 | else { 99 | self.moduleFilename = nil 100 | } 101 | } 102 | 103 | /// Print the arguments. 104 | func dump() { 105 | print("Config:", configURL.path) 106 | print("Target:", targetName) 107 | print("Input: ", inputURLs.map(\.path)) 108 | print("Output:", outputURL.path) 109 | print("Stem: ", stem) 110 | } 111 | 112 | private static func usage(_ toolName: String) { 113 | print( 114 | """ 115 | Usage: \(toolName) 116 | """ 117 | ) 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /Plugins/Tools/sqlite2swift/ExitCodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /// The exit codes the process uses. 7 | enum ExitCodes: Int32, Swift.Error { 8 | case ok = 0 9 | case invalidArguments = 1 10 | case couldNotOpenConfiguration = 2 11 | case couldNotWriteOutput = 3 12 | case couldNotLoadInMemorySchema = 4 13 | } 14 | -------------------------------------------------------------------------------- /Plugins/Tools/sqlite2swift/README.md: -------------------------------------------------------------------------------- 1 |

Tool: sqlite2swift 2 | 4 |

5 | 6 | This tool generates code for SQLite databases and/or a set of SQL source files 7 | creating a SQLite database. 8 | 9 | It is run by the `Enlighter` plugin for each applicable set of files it finds, 10 | but can also be used as a separate tool. 11 | 12 | 13 | ### Who 14 | 15 | Lighter is brought to you by 16 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 17 | We like feedback, GitHub stars, cool contract work, 18 | presumably any form of praise you can think of. 19 | -------------------------------------------------------------------------------- /Plugins/Tools/sqlite2swift/SQLite2Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | import LighterGeneration 8 | import LighterCodeGenAST 9 | import SQLite3Schema 10 | 11 | /// This is a tool to generate the Swift to represent one single SQL database, 12 | /// though that database can be composed from different input databases and/or 13 | /// SQL source files. 14 | struct SQLite2Swift { 15 | 16 | let args : Arguments 17 | var config = LighterConfiguration.default 18 | 19 | /// Parse the arguments and initialize the tool. 20 | init(_ arguments: [ String ]) throws { 21 | args = try Arguments() 22 | } 23 | 24 | /// Start the tool logic. 25 | mutating func run() throws { 26 | if args.verbose { 27 | args.dump() 28 | } 29 | 30 | if args.configURL.scheme != "default" { 31 | try loadConfiguration() 32 | } 33 | 34 | if args.verbose { 35 | print("Config Values:", config) 36 | } 37 | 38 | let schema = try loadSchema(from: args.inputURLs) 39 | if args.verbose { 40 | print("User Version:", schema.userVersion) 41 | } 42 | 43 | let database = buildGenerationModel(for: schema) 44 | if args.verbose { 45 | print("DB Info:", database) 46 | } 47 | 48 | let unit = generateCombinedFile(for: database) 49 | 50 | try writeToOutput(unit) 51 | 52 | #if DEBUG && false 53 | dumpOutputFile() 54 | #endif 55 | 56 | if args.verbose { 57 | print("Wrote to:", args.outputURL.path) 58 | } 59 | } 60 | 61 | 62 | // MARK: - Implementation 63 | 64 | private mutating func loadConfiguration() throws { 65 | do { 66 | config = try LighterConfiguration( 67 | contentsOf : args.configURL, 68 | for : args.targetName, 69 | stem : args.stem 70 | ) 71 | } 72 | catch { 73 | print("Failed to load config from:\n ", args.configURL.path, 74 | "\n error:", error) 75 | throw ExitCodes.couldNotOpenConfiguration 76 | } 77 | } 78 | 79 | private func loadSchema(from inputURLs: [ URL ]) throws -> Schema { 80 | do { return try SchemaLoader.buildSchemaFromURLs(args.inputURLs) } 81 | catch { 82 | print("Could not fetch schema:", error) 83 | throw ExitCodes.couldNotLoadInMemorySchema 84 | } 85 | } 86 | 87 | private func buildGenerationModel(for schema: Schema) -> DatabaseInfo { 88 | let dbInfo = DatabaseInfo(name: args.stem, schema: schema) 89 | let fancifier = Fancifier(options: config.swiftMapping) 90 | fancifier.fancifyDatabaseInfo(dbInfo) 91 | return dbInfo 92 | } 93 | 94 | private func generateCombinedFile(for database: DatabaseInfo) 95 | -> CompilationUnit 96 | { 97 | let gen = EnlighterASTGenerator( 98 | database : database, 99 | filename : args.outputURL.lastPathComponent, 100 | options : config.codeGeneration 101 | ) 102 | return gen.generateCombinedFile(moduleFileName: args.moduleFilename) 103 | } 104 | 105 | private func writeToOutput(_ unit: CompilationUnit) throws { 106 | do { 107 | try unit.writeCode(to: args.outputURL, 108 | configuration: config, 109 | headerName: args.toolName) 110 | } 111 | catch { 112 | print("Failed to write to", args.outputURL.path, "error:", error) 113 | throw ExitCodes.couldNotWriteOutput 114 | } 115 | } 116 | 117 | private func dumpOutputFile() throws { 118 | let string = try String(contentsOf: args.outputURL) 119 | print("Generated:\n-----\n\(string)\n-----") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Plugins/Tools/sqlite2swift/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | // This is a tool to generate the Swift to represent one single SQL database, 7 | // though that database can be composed from different input databases and/or 8 | // SQL source files. 9 | 10 | import func Foundation.exit 11 | 12 | do { 13 | var tool = try SQLite2Swift(CommandLine.arguments) 14 | try tool.run() 15 | } 16 | catch let error as ExitCodes { 17 | exit(error.rawValue) 18 | } 19 | catch { 20 | print("Unexpected error:", error) 21 | exit(42) 22 | } 23 | -------------------------------------------------------------------------------- /Plugins/WriteInternalVariadics/README.md: -------------------------------------------------------------------------------- 1 |

SPM Command Plugin: WriteInternalVariadics 2 | 4 |

5 | 6 | This is a plugin that is used as part of this package only (i.e. it is not 7 | a product). 8 | 9 | The command needs to be run if the `Lighter.json` settings are changed, 10 | or the API changes. 11 | It is intentionally not a `BuildToolPlugin` (which also works, but hides the 12 | generated file, which we don't want). 13 | 14 | It is NOT necessary for embedded lighter setups, which can customize the 15 | settings on a per target basis as required. 16 | 17 | It calls into the `GenerateInternalVariadics` tool to generate the 18 | "Lighter/Operations/GeneratedVariadicOperations.swift" file. 19 | 20 | Which contains the variadic functions for `select`, `update` etc, e.g.: 21 | ```swift 22 | @inlinable 23 | func select( 24 | from tableOrView: KeyPath, 25 | _ column1: KeyPath, 26 | _ column2: KeyPath, 27 | orderBy sortColumn: KeyPath, 28 | _ direction: SQLSortOrder = .ascending, 29 | _ limit: Int? = nil 30 | ) async throws -> [ ( column1: C1.Value, column2: C2.Value ) ] 31 | where C1: SQLColumn, C2: SQLColumn, CS: SQLColumn, T == C1.T, T == C2.T, T == CS.T 32 | ``` 33 | 34 | 35 | ### Who 36 | 37 | Lighter is brought to you by 38 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 39 | We like feedback, GitHub stars, cool contract work, 40 | presumably any form of praise you can think of. 41 | -------------------------------------------------------------------------------- /Plugins/WriteInternalVariadics/WriteInternalVariadics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | import PackagePlugin 7 | import Foundation 8 | // Note: Plugins cannot use libs in the same package! 9 | 10 | @main 11 | struct WriteInternalVariadics: CommandPlugin { 12 | 13 | enum WriteInternalVariadicsError: Swift.Error { 14 | case couldNotFindLighter 15 | case couldNotFindConfigurationFile(String) 16 | case toolRunFailed(Int) 17 | } 18 | 19 | func performCommand(context: PackagePlugin.PluginContext, 20 | arguments: [ String ]) async throws 21 | { 22 | // executed in the package root dir 23 | // Selected targets in Xcode are passed in like this: 24 | // ["--target", "Lighter"] 25 | let targetName = "Lighter" // could be made configurable/honor the arguments 26 | 27 | guard let target = 28 | try context.package.targets(named: [ targetName ]) 29 | .first as? SwiftSourceModuleTarget else 30 | { 31 | throw WriteInternalVariadicsError.couldNotFindLighter 32 | } 33 | 34 | // Generate 35 | 36 | try generate(context: context, target: target) 37 | } 38 | 39 | private func generate(context: PackagePlugin.PluginContext, 40 | target: SwiftSourceModuleTarget) throws 41 | { 42 | // Lookup Configuration (init is per-target) 43 | 44 | #if compiler(>=6) 45 | let configURL = context.package.directoryURL 46 | .appending(component: "Ligher.json") 47 | guard FileManager.default.isReadableFile(atPath: configURL.path) else { 48 | throw WriteInternalVariadicsError 49 | .couldNotFindConfigurationFile(configURL.path) 50 | } 51 | 52 | let operationsFolder = target.directoryURL 53 | .appending(component: "Operations", directoryHint: .isDirectory) 54 | let outputFile = "GeneratedVariadicOperations.swift" 55 | let outputURL = operationsFolder.appending(component: outputFile) 56 | #else 57 | let configPath = context.package.directory 58 | .appending(subpath: "Lighter.json") 59 | guard FileManager.default.isReadableFile(atPath: configPath.string) else { 60 | throw WriteInternalVariadicsError 61 | .couldNotFindConfigurationFile(configPath.string) 62 | } 63 | 64 | let operationsFolder = target 65 | .directory.appending(subpath: "Operations") 66 | let outputFile = "GeneratedVariadicOperations.swift" 67 | let outputPath = operationsFolder.appending(subpath: outputFile) 68 | let configURL = URL(fileURLWithPath: configPath.string) 69 | let outputURL = URL(fileURLWithPath: outputPath.string) 70 | #endif 71 | 72 | let targetName = target.name 73 | 74 | #if DEBUG || true 75 | let debugFH = fopen("/tmp/zzdebug.log", "w") 76 | if let debugFH = debugFH { 77 | fputs("Start \(Date())", debugFH) 78 | fclose(debugFH) 79 | } 80 | #endif // DEBUG || true 81 | 82 | // Lookup generator tool 83 | 84 | let tool = try context.tool(named: "GenerateInternalVariadics") 85 | 86 | let process = Process() 87 | #if compiler(>=6) && canImport(Foundation) 88 | process.executableURL = tool.url 89 | #else 90 | process.executableURL = URL(fileURLWithPath: tool.path.string) 91 | #endif 92 | process.arguments = [ configURL.path, targetName, outputURL.path ] 93 | 94 | try process.run() 95 | process.waitUntilExit() 96 | 97 | if process.terminationStatus != 0 { 98 | throw WriteInternalVariadicsError.toolRunFailed( 99 | Int(process.terminationStatus)) 100 | } 101 | else { 102 | print("Wrote to:", outputURL.path) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Lighter/ConnectionHandlers/UnsafeReuse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import func SQLite3.sqlite3_close 7 | import let SQLite3.SQLITE_FAIL 8 | import struct Foundation.URL 9 | 10 | extension SQLConnectionHandler { 11 | 12 | /** 13 | * The `UnsafeReuse` handler just vends the same connection. 14 | * 15 | * This is used in transactions, which have a single connection assigned, 16 | * and for testing scenarios. 17 | * It is only applicable if operations are executed in a strictly serial 18 | * manner. 19 | */ 20 | public final class UnsafeReuse: SQLConnectionHandler, @unchecked Sendable { 21 | 22 | public private(set) var handle : OpaquePointer? 23 | public private(set) var openConfiguration : Configuration? 24 | public let closeOnDeinit : Bool 25 | 26 | /// Initialize a new UnsafeReuse handler. 27 | public init(url: URL, handle: OpaquePointer? = nil, 28 | closeOnDeinit: Bool = false) 29 | { 30 | self.closeOnDeinit = closeOnDeinit 31 | self.handle = handle 32 | super.init(url: url) 33 | } 34 | deinit { 35 | if closeOnDeinit, let handle = handle { 36 | sqlite3_close(handle) 37 | self.handle = nil 38 | } 39 | } 40 | 41 | /// Clear the handle w/o closing the database. 42 | public func clear() { 43 | handle = nil 44 | } 45 | /// Close the database and clear the handle 46 | public func close() { 47 | if let handle = handle { sqlite3_close(handle) } 48 | clear() 49 | } 50 | 51 | override public func openConnection(_ configuration: Configuration) throws 52 | -> OpaquePointer 53 | { 54 | guard let handle = handle else { 55 | throw LighterError(.couldNotOpenDatabase(url), SQLITE_FAIL, 56 | "Unsafe handle got cleared.") 57 | } 58 | assert(openConfiguration == nil, 59 | "Attempt to open an unsafe-reuse connection a second time!") 60 | openConfiguration = configuration 61 | return handle 62 | } 63 | 64 | override public func releaseConnection(_ connection : OpaquePointer?, 65 | with configuration : Configuration, 66 | afterError error : Error? = nil) 67 | { 68 | guard let connection = connection else { 69 | assert(connection != nil, 70 | "Attempt to release an nil connection") 71 | return 72 | } 73 | guard connection == handle else { 74 | assert(connection == handle, 75 | "Attempt to release an incorrect unsafe handle?") 76 | return 77 | } 78 | if let openConfiguration = openConfiguration { 79 | assert(openConfiguration == configuration, 80 | "The release configuration differs from active one?") 81 | } 82 | else { 83 | assertionFailure("Found no open configuration on release?") 84 | } 85 | 86 | openConfiguration = nil 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sources/Lighter/Database/SQLDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | // Later: Rework to use String paths by default and only resort to URLs in 7 | // a canImport section. 8 | import struct Foundation.URL 9 | 10 | /** 11 | * A type representing a SQLite3 database. 12 | * 13 | * Besides type information this includes top-level operations that can be 14 | * performed as part of the ``SQLDatabaseOperations`` protocol (and its 15 | * associated protocols). 16 | */ 17 | public protocol SQLDatabase: SQLDatabaseOperations { 18 | 19 | #if swift(>=5.7) 20 | /// Returns all ``SQLRecord`` type objects associated with the database. 21 | static var _allRecordTypes : [ any SQLRecord.Type ] { get } 22 | #endif 23 | 24 | /** 25 | * Initialize a database with a ``SQLConnectionHandler``. 26 | * 27 | * Connection handlers deal with opening and pooling database handles. 28 | * 29 | * Example: 30 | * ```swift 31 | * MyDatabase(connectionHandler: .simplePool(url: url, readOnly: readOnly)) 32 | * ``` 33 | * 34 | * - Parameters: 35 | * - connectionHandler: The handler that should be used w/ the database. 36 | */ 37 | init(connectionHandler: SQLConnectionHandler) 38 | 39 | /** 40 | * Initialize a database with a `URL`. 41 | * 42 | * This opens a database using the ``SQLConnectionHandler/SimplePool`` handler. 43 | * That handler does simple connection pooling so that excessive open 44 | * operations are avoided. 45 | * 46 | * Example: 47 | * ```swift 48 | * let db = MyDatabase( 49 | * url: bundle.url(forResource: "Contacts", withExtension: "db")!, 50 | * readOnly: false 51 | * ) 52 | * ``` 53 | * 54 | * - Parameters: 55 | * - url: The filesystem `URL` to a SQLite3 database file. 56 | * - readOnly: Whether the database should be opened read-only. 57 | */ 58 | init(url: URL, readOnly: Bool) 59 | 60 | /** 61 | * Create or open the SQL database at the given URL. 62 | * 63 | * If a file already exists at the URL, the database structure is initialized 64 | * with that. 65 | * Otherwise, the `databaseFileURL` is copied to the `url` destination 66 | * (using `FileManager.copyItem(at:to:)`). 67 | * 68 | * Example: 69 | * ```swift 70 | * let db = try Contacts.create( 71 | * at: destinationURL, 72 | * readOnly: false, 73 | * copying: bundle.url(forResource: "Contacts", withExtension: "db")! 74 | * ) 75 | * ``` 76 | * 77 | * - Parameters: 78 | * - url: The place where the database should be created. 79 | * - readOnly: Whether the database should be opened read-only. 80 | * - overwrite: Whether the database should be deleted if it 81 | * exists already (useful during development). 82 | * - databaseFileURL: The "source" database to be copied. 83 | */ 84 | static func bootstrap(at url: URL, readOnly: Bool, overwrite: Bool, 85 | copying databaseFileURL: URL) throws -> Self 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Lighter/Database/SQLDatabaseTesting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import SQLite3 7 | import struct Foundation.URL 8 | 9 | // Note: Things in here are INTENTIONALLY internal, so that can be only used 10 | // in `@testable` imports. They are NOT for general consumption. 11 | 12 | 13 | internal extension SQLDatabase { 14 | 15 | /// Returns an database which loads the passed in URL into memory, so that 16 | /// tests can't be applied on it. 17 | /// This is ONLY intended for testing and requires a `@testable import`. 18 | static func loadIntoMemoryForTesting(_ url: URL) -> Self { 19 | let testDB = zqlite3_load_into_memory(url.path) 20 | return Self(connectionHandler: 21 | .unsafeReuse(testDB, url: url, closeOnDeinit: true)) 22 | } 23 | 24 | /// This is ONLY intended for testing and requires a `@testable import`. 25 | var testDatabaseHandle : OpaquePointer! { 26 | guard let ch = connectionHandler as? SQLConnectionHandler.UnsafeReuse else { 27 | assertionFailure("Unexpected access to test database handle!") 28 | return nil 29 | } 30 | return ch.handle 31 | } 32 | } 33 | 34 | /** 35 | * Copies the SQLite database at the path into a SQLite in-memory database. 36 | * 37 | * - Returns: The db handle to the in-memory database, if successful. 38 | */ 39 | internal func zqlite3_load_into_memory(_ path: String) -> OpaquePointer? { 40 | var db : OpaquePointer? 41 | 42 | guard sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else 43 | { 44 | return nil 45 | } 46 | defer { sqlite3_close(db) } 47 | 48 | var memDB: OpaquePointer? 49 | guard sqlite3_open_v2(":memory:", &memDB, SQLITE_OPEN_READWRITE, 50 | nil) == SQLITE_OK else 51 | { 52 | assertionFailure("Memdb open failed") 53 | return nil 54 | } 55 | 56 | guard let backup = sqlite3_backup_init(memDB, "main", db, "main") else { 57 | assertionFailure("Backup init failed") 58 | sqlite3_close(memDB) 59 | return nil 60 | } 61 | 62 | guard sqlite3_backup_step(backup, -1) == SQLITE_DONE /* 101 */ else { 63 | assertionFailure("Backup failed") 64 | sqlite3_backup_finish(backup) 65 | sqlite3_close(memDB) 66 | return nil 67 | } 68 | 69 | if sqlite3_errcode(memDB) != SQLITE_OK { 70 | if let backupError = sqlite3_errmsg(memDB).flatMap(String.init(cString:)) { 71 | assertionFailure("Backup failed: \(backupError)") 72 | } 73 | else { 74 | assertionFailure("Backup failed.") 75 | } 76 | sqlite3_backup_finish(backup) 77 | sqlite3_close(memDB) 78 | return nil 79 | } 80 | 81 | sqlite3_backup_finish(backup) 82 | return memDB 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Lighter/Expression/SQLExpression.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A raw SQL expression, that can use string interpolation to generate 8 | * proper values. 9 | * 10 | * Example: 11 | * ```swift 12 | * "SELECT * FROM person WHERE \($0.personId) LIKE UPPER(\(name))" 13 | * ``` 14 | */ 15 | public struct SQLExpression: ExpressibleByStringInterpolation { 16 | 17 | @usableFromInline 18 | let interpolation : SQLInterpolation 19 | 20 | @inlinable 21 | public init(stringInterpolation interpolation: SQLInterpolation) { 22 | self.interpolation = interpolation 23 | } 24 | @inlinable 25 | public init(stringLiteral sql: String) { 26 | self.interpolation = SQLInterpolation(verbatim: sql) 27 | } 28 | 29 | 30 | // MARK: - SQL Generation 31 | 32 | public func generateSQL(into builder: inout SQLBuilder) { 33 | interpolation.generateSQL(into: &builder) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Lighter/Expression/SQLInterpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | public struct SQLInterpolation: StringInterpolationProtocol, Sendable { 7 | 8 | @usableFromInline 9 | enum Fragment: Sendable { 10 | // Would be nice to support columns, but those would make the fragment 11 | // generic and non-hashable? 12 | case raw (String) 13 | case parameter(SQLiteValueType) 14 | } 15 | 16 | @usableFromInline 17 | var fragments = [ Fragment ]() 18 | 19 | @inlinable 20 | public init(literalCapacity: Int, interpolationCount: Int) { 21 | fragments.reserveCapacity(interpolationCount) 22 | } 23 | 24 | public init(verbatim sql: String) { 25 | fragments = [ .raw(sql) ] 26 | } 27 | 28 | // MARK: - Literals 29 | 30 | @inlinable 31 | public mutating func appendLiteral(_ sql: String) { 32 | guard !sql.isEmpty else { return } 33 | if case .raw(let lastSQL) = fragments.last { 34 | fragments.removeLast() 35 | fragments.append(.raw(lastSQL + sql)) 36 | } 37 | else { 38 | fragments.append(.raw(sql)) 39 | } 40 | } 41 | 42 | 43 | // MARK: - Interpolations 44 | 45 | @inlinable 46 | public mutating func appendInterpolation(_ value: Int) { 47 | appendLiteral(String(value)) 48 | } 49 | @inlinable 50 | public mutating func appendInterpolation(_ value: Double) { 51 | appendLiteral(String(value)) 52 | } 53 | 54 | @inlinable 55 | public mutating func appendInterpolation(verbatim sql: String) { 56 | appendLiteral(sql) 57 | } 58 | 59 | @inlinable 60 | public mutating func appendInterpolation(_ data: [ UInt8 ]) { 61 | fragments.append(.parameter(data)) 62 | } 63 | @inlinable 64 | public mutating func appendInterpolation(_ data: S) 65 | where S: Sequence, S.Element == UInt8 66 | { 67 | appendInterpolation([ UInt8 ](data)) 68 | } 69 | 70 | @inlinable 71 | public mutating func appendInterpolation(_ string: String) { 72 | fragments.append(.parameter(string)) 73 | } 74 | @inlinable 75 | public mutating func appendInterpolation(_ string: Substring) { 76 | self.appendInterpolation(String(string)) 77 | } 78 | 79 | @inlinable 80 | public mutating func appendInterpolation(_ table: T.Type) { 81 | fragments.append(.raw("\"" + table.Schema.externalName + "\"")) 82 | } 83 | @inlinable 84 | public mutating func appendInterpolation(_ column: C) { 85 | // Not ideal, but we'd need to type erase? 86 | fragments.append(.raw("\"" + column.externalName + "\"")) 87 | } 88 | 89 | @inlinable 90 | public mutating func appendInterpolation(_ value: SQLiteValueType) { 91 | if value.requiresSQLBinding { 92 | fragments.append(.parameter(value)) 93 | } 94 | else { 95 | appendLiteral(value.sqlStringValue) 96 | } 97 | } 98 | 99 | @inlinable 100 | public mutating func appendInterpolation(_ value: V) { 101 | if value.requiresSQLBinding { 102 | fragments.append(.parameter(value)) 103 | } 104 | else { 105 | appendLiteral(value.sqlStringValue) 106 | } 107 | } 108 | } 109 | 110 | extension SQLInterpolation { 111 | 112 | public func generateSQL(into builder: inout SQLBuilder) { 113 | for fragment in fragments { 114 | switch fragment { 115 | 116 | case .raw(let sqlFragment): 117 | builder.append(sqlFragment) 118 | 119 | case .parameter(let value): 120 | builder.append(" ") 121 | builder.append(builder.sqlString(for: value)) 122 | builder.append(" ") 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | A collection of questions and possible answers. 4 | 5 | ## Overview 6 | 7 | Any question we should add: [info@zeezide.de](mailto:info@zeezide.de). 8 | 9 | 10 | ## Object Relational Mappers (ORM) 11 | 12 | Relationship to [ORMs](https://en.wikipedia.org/wiki/Object–relational_mapping). 13 | 14 | ### What is different to an ORM? 15 | 16 | An **Object** Relational Mapper usually maintains a graph of interconnected 17 | objects (as objects, reference types). 18 | 19 | Lighter doesn't do any objects, the Lighter generated model types are structures 20 | that directly map to the SQLite tables and views, in a typesafe manner. 21 | 22 | While it can also generate fetchers for relationships, 23 | it doesn't represent such within the models. 24 | For example a Northwind `Order` model doesn't have an associated `product` 25 | reference property 26 | (Lighter _does_ generate the `db.products.fetch(for: order)` fetch function 27 | though). 28 | 29 | Lighter also doesn't do any caching or invalidation, i.e. no 30 | "managed object context". Lighter record structures are context free (i.e. also 31 | do not refer to the database in any way). 32 | 33 | 34 | ### Is Lighter a replacement for CoreData? 35 | 36 | It can be, but it is a lot lower level than CoreData. Which can be an advantage, 37 | especially for performance. It also keeps things simple, less surprises. 38 | 39 | CoreData is a rich framework that goes as far as providing direct SwiftUI 40 | integration. 41 | Lighter doesn't do anything like that **intentionally**. It is supposed to be 42 | _lighter_. 43 | (Stay tuned for Heavier™️.) 44 | 45 | 46 | ### Is Lighter a replacement for ZeeQL? 47 | 48 | The [ZeeQL](https://zeeql.io) situation is similar to CoreData. 49 | 50 | 51 | ## SQLite Access Libraries 52 | 53 | A set of SQLite Swift libraries are already available. 54 | The most popular one is 55 | [GRDB.swift](https://github.com/groue/GRDB.swift), 56 | another is SQLite.swift. ORMs like 57 | ZeeQL also provide low level SQLite access. 58 | 59 | ### What are advantages over SQLite.swift or GRDB? 60 | 61 | Lighter has two main advantages over "regular" libraries: 62 | 1. It pre-generates fully typesafe (down to the SQL schema) structures for 63 | all tables, one doesn't have to type out the structures manually. 64 | 2. It pre-generates common queries in highly efficient code, avoiding 65 | a lot of runtime "mapping overhead" and a lot of temporary allocations. 66 | 67 | Both libraries provide `Codable` support that can somewhat mirror the generated 68 | structures (i.e. provide more convenience). 69 | We found Codable to be extraordinarily slow (e.g. >19× slower when using 70 | SQLite.swift). 71 | 72 | - 73 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Lighter.md: -------------------------------------------------------------------------------- 1 | # ``Lighter`` 2 | 3 | Type-safe down to the schema. Very, **very**, fast. Dependency free. 4 | 5 | @Metadata { 6 | @DisplayName("Lighter.swift for SQLite3") 7 | } 8 | 9 | ## Overview 10 | 11 | **Lighter** is a Swift toolset to work with [SQLite3](https://www.sqlite.org) 12 | databases in a way that is **typesafe** not just on the Swift side, 13 | but **down to the SQL schema**. 14 | Like [SwiftGen](https://github.com/SwiftGen/SwiftGen) but for SQLite. 15 | It is **not an ORM**, it doesn't do type mapping at runtime. 16 | 17 | Like SQLite, the Lighter toolset is very versatile, highly configurable, and 18 | applicable to a wide range of applications and project setups. 19 | From caches in iOS apps, or documents in Mac apps, to server side datasets. 20 | It isn't just a single tool, it is a set of tools for different usage scenarios. 21 | 22 | In short, it takes SQL (or binary SQLite) files like this: 23 | ```sql 24 | CREATE TABLE person ( 25 | person_id INTEGER PRIMARY KEY NOT NULL, 26 | name TEXT NOT NULL, 27 | title TEXT NULL 28 | ); 29 | ``` 30 | and generates Swift code like this: 31 | ```swift 32 | struct ContactsDB { 33 | 34 | struct Person: Identifiable, Hashable, Codable { 35 | var id : Int 36 | var name : String 37 | var title : String? 38 | } 39 | } 40 | ``` 41 | Alongside the necessary code to work with the tables in the database: 42 | ```swift 43 | let people = try await db.people.fetch { $0.name.hasPrefix("Bour") } 44 | var jason = people[0] 45 | jason.name = "Bourne!" 46 | try await db.transaction { tx in 47 | try tx.update(jason) 48 | try tx.insert(jason) 49 | try tx.delete(jason) 50 | } 51 | ``` 52 | It works with async/await, or without, 53 | with a supporting library (Lighter), or dependency free. 54 | Lighter knows about relationships contained in the database and can also 55 | generate code to resolve those: 56 | ```swift 57 | let product = try await db.products.find(42) 58 | let orders = try await db.orders.fetch(for: product) 59 | ``` 60 | If desired, it generates beautiful DocC documentation within the generated code 61 | ([Example](https://Northwind-swift.github.io/NorthwindSQLite.swift/documentation/northwind/employee)). 62 | 63 | 64 | ## Lighter Toolkit Components 65 | 66 | The toolkit consists of four major parts: 67 | - The “Lighter” **support library** (only intended to be used in combination with 68 | generated code, not as a standalone library). It is **optional**. 69 | - The “Enlighter” Swift 5.6 **build plugin** 70 | for Xcode and the Swift Package Manager. 71 | - The “Generate Code for SQLite3” Swift 5.6 **command plugin** 72 | for Xcode and Swift Package Manager 73 | - The “sqlite2swift” **tool** that can be used to generate SQLite code if the 74 | environment doesn't allow for Xcode 14 or Swift 5.6 yet (the generated code 75 | runs against earlier version). 76 | 77 | There is also the the 78 | [“Code for SQLite3” application](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010). 79 | A macOS app that does the same like “sqlite2swift”, but as an app. 80 | If you want to support this project, consider buying a copy. Thank you! 81 | 82 | There are two associated repositories: 83 | - [Examples](https://github.com/Lighter-swift/Examples/): 84 | Contains examples on how to use the toolset, including a few SwiftUI 85 | applications and even a server side API. 86 | - [NorthwindSQLite.swift](https://github.com/Northwind-swift/NorthwindSQLite.swift): 87 | A version of the Microsoft Access 2000 Northwind sample database, 88 | re-engineered for SQLite3, and packaged up as a Swift package 89 | ([DocC Documentation](https://Northwind-swift.github.io/NorthwindSQLite.swift/documentation/northwind/))! 90 | 91 | 92 | ## Topics 93 | 94 | ### Getting Started 95 | 96 | - 97 | - 98 | - 99 | 100 | ### Advanced 101 | 102 | - 103 | - 104 | - 105 | - 106 | - 107 | - 108 | - 109 | 110 | ### Support 111 | 112 | - 113 | - 114 | - 115 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Linux.md: -------------------------------------------------------------------------------- 1 | # Linux 2 | 3 | Using Lighter on Linux. 4 | 5 | ## Overview 6 | 7 | Lighter itself, and the generated code, works on Linux. 8 | It can be a useful option when shipping large read-only datasets alongside an 9 | associated endpoint, or for applications that do not require multiple servers. 10 | SQLite is astonishingly fast and easy to deploy, it can be a great alternative 11 | to more complex setups. 12 | 13 | Unlike macOS, Linux Swift setups do not bundle the `SQLite3` module. 14 | Lighter includes one for maximum convenience. 15 | 16 | During compilation the `libsqlite3` Linux dev package must be available, 17 | e.g. on Debian or Ubuntu systems it can be installed using: 18 | ```sh 19 | apt-get update 20 | apt-get install libsqlite3-dev 21 | ``` 22 | 23 | ### Example 24 | 25 | [NorthwindWebAPI](https://github.com/Lighter-swift/Examples/tree/develop/Sources/NorthwindWebAPI/) 26 | is a small server side Swift example exposing the DB as a JSON API endpoint, 27 | and providing a few pretty HTML pages showing data contained. 28 | 29 | Example server: 30 | ```swift 31 | #!/usr/bin/swift sh 32 | import MacroExpress // @Macro-swift 33 | import Northwind // @Northwind-swift/NorthwindSQLite.swift 34 | 35 | let db = Northwind.module! 36 | let app = express() 37 | 38 | app.get("/api/products") { _, res in 39 | res.send(try db.products.fetch()) 40 | } 41 | 42 | app.listen(1337) // start server 43 | ``` 44 | 45 | Example `Package.swift`: 46 | ```swift 47 | // swift-tools-version:5.7 48 | import PackageDescription 49 | 50 | var package = Package( 51 | name: "LighterExamples", 52 | 53 | platforms: [ .macOS(.v10_15), .iOS(.v13) ], 54 | products: [ 55 | .executable(name: "NorthwindWebAPI", targets: [ "NorthwindWebAPI" ]) 56 | ], 57 | 58 | dependencies: [ 59 | .package(url: "https://Lighter-swift/Lighter.git", from: "1.0.2"), 60 | .package(url: "https://Northwind-swift/NorthwindSQLite.swift.git", 61 | from: "1.0.0"), 62 | .package(url: "https://github.com/Macro-swift/MacroExpress.git", 63 | from: "1.0.2") 64 | ], 65 | 66 | targets: [ 67 | .executableTarget( 68 | name: "NorthwindWebAPI", 69 | dependencies: [ 70 | .product(name: "Northwind", package: "NorthwindSQLite.swift"), 71 | "MacroExpress" 72 | ], 73 | exclude: [ "views", "README.md" ] 74 | ) 75 | ] 76 | ) 77 | ``` 78 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Manual.md: -------------------------------------------------------------------------------- 1 | # Manual Generation 2 | 3 | How to generate Lighter content by hand. 4 | 5 | ## Overview 6 | 7 | The easiest way to use Lighter is to use the "Enlighter" build tool plugin 8 | which is automatically run by Swift Package Manager or Xcode (14+) if the 9 | database files change. 10 | But if that isn't an option (e.g. Xcode 14 can't be used yet), Lighter 11 | provides a set of options to do it otherwise. 12 | 13 | 14 | ### Using the "Generate Code for SQLite" command plugin. 15 | 16 | Command plugins are new Swift 5.6 feature that allows running commands embedded 17 | in packages manually (vs automatically as with "build tool plugins" like 18 | Enlighter). 19 | 20 | "Generate Code for SQLite" is a command plugin which looks at the database/SQL 21 | files just like Enlighter and produces Swift files for them. Unlike Enlighter 22 | it doesn't write them into the build path, but into the sources of the project 23 | or package (SPM/Xcode is prompting to allow for that). 24 | 25 | In Xcode 14 the plugin can be run from the "File / Packages / Plugin" menu. 26 | Note that Xcode 14 is only needed at generation time for that. Afterwards the 27 | file can be checked into the repository as source code. 28 | 29 | Even with Xcode 13 the command plugin can be run using swift on the commandline. 30 | 31 | > Note: Unless working on packages, Xcode doesn't automatically add the 32 | > generate Swift file. 33 | > So on first run, one may need to look into the filesystem and drag the 34 | > generated file into the project. 35 | 36 | 37 | ### Using the "Code for SQLite3" Application 38 | 39 | [“Code for SQLite3” application](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010) 40 | is a macOS app that does the same like the “`sqlite2swift`” tool, but as an app. 41 | It can be useful to play w/ the Lighter options and to manually generate the 42 | code. 43 | 44 | Just drag the database files into the app, and it'll generate the Swift code. 45 | Which can then be copy/pasted into the Xcode project. 46 | 47 | 48 | ### Using the `sqlite2swift` tool manually 49 | 50 | This is a useful option if Swift 5.6 is not yet available in a CI environment, 51 | but the code should be automatically generated. The tool can be added as a 52 | traditional script phase to a project. 53 | 54 | The arguments: 55 | - 1st argument: The or `-` if there is none. 56 | - 2nd argument: The name of the target the code is generated for, this is 57 | used to resolve configuration options in the file. 58 | - other arguments: The database/SQL input files. 59 | - last argument: The Swift output file. 60 | 61 | `sqlite2swift` is shipped as a tool product in the Lighter package, and should 62 | compile with Swift 5.0 and beyond. 63 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Northwind.md: -------------------------------------------------------------------------------- 1 | # Northwind 2 | 3 | Using the Northwind example database. 4 | 5 | ## Overview 6 | 7 | The Northwind database is a common database example that has been ported 8 | to SQLite. 9 | Lighter provides a Swift version of that in the 10 | [NorthwindSQLite.swift](https://github.com/Northwind-swift/NorthwindSQLite.swift) 11 | repository. 12 | 13 | > Note: The particular SQLite version of the Northwind database is quite 14 | > lacking. For example booleans are stored as TEXTs, many columns are 15 | > inappropriately marked as `NULL`able.
16 | > That actually makes it a good example on how to deal with such databases in 17 | > Lighter. 18 | 19 | The Swift Northwind API: [Documentation](https://Northwind-swift.github.io/NorthwindSQLite.swift/documentation/northwind/). 20 | 21 | ## Demos 22 | 23 | Examples based on the [Northwind](https://Northwind-swift.github.io/NorthwindSQLite.swift/documentation/northwind/) Database: 24 | 25 | - [NorthwindWebAPI](https://github.com/Lighter-swift/Examples/tree/develop/Sources/NorthwindWebAPI/) 26 | (A server side Swift example 27 | exposing the DB as a JSON API endpoint, and providing a few pretty HTML 28 | pages showing data contained.) 29 | 30 | - [NorthwindSwiftUI](https://github.com/Lighter-swift/Examples/tree/develop/Sources/NorthwindSwiftUI/) 31 | (A SwiftUI example that lets 32 | one browse the Northwind database. Uses the Lighter API in combination and 33 | its async/await supports.) 34 | 35 | ## Getting Started 36 | 37 | Add the `https://Northwind-swift.github.io/NorthwindSQLite.swift` as a package 38 | dependency to a Swift Package Manager package. 39 | 40 | Then just import `Northwind` and run queries against the database: 41 | ```swift 42 | import Northwind 43 | 44 | let db = Northwind.module! 45 | for product in try db.products.fetch() { 46 | print("Product:", product.id, product.productName) 47 | } 48 | ``` 49 | 50 | > Note: It looks like Xcode 14b5 still has an issue when adding Northwind 51 | > (or any package w/ embedded databases) to an Xcode package root. 52 | > In those cases a "local package" can be used to make it work. 53 | > Apple has documented the process in: 54 | > [Organizing Your Code with Local Packages](https://developer.apple.com/documentation/xcode/organizing-your-code-with-local-packages). 55 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | A quick look at Lighter performance. 4 | 5 | ## Overview 6 | 7 | Lighter comes with a small 8 | [performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite). 9 | It uses the (larger, populated) 10 | [Northwind SQLite](https://github.com/jpwhite3/northwind-SQLite3/) 11 | database as the base set. 12 | It doesn't have the goal to be scientifically proper. 13 | 14 | ### Results 15 | 16 | The suite evaluates the load performance on queries against the "Orders" table, 17 | which has 16K records. The test loads all 16K records on each iteration. 18 | 19 | Lighter is expected to perform excellent, as it directly/statically binds 20 | SQLite prepared statements for the generated structures. 21 | 22 | M1 Mini with 10 rampup iterations and 500 test iterations. 23 | ``` 24 | Orders.fetchAll setup rampup duration 25 | Enlighter SQLite 0 0,135s 6,629s ~20% faster than Lighter (75/s) 26 | Lighter 0 0,162s 7,927s Baseline (63/s) 27 | GRDB - - ~12s Handwritten Mapping 28 | SQLite.swift 0 0,613s 30,643s Handwritten Mapping (>3× slower) (16/s) 29 | GRDB 0,001 0,995s 49,404s Codable (>6× slower) (10/s) 30 | SQLite.swift 0,001 3,109s 153,172s Codable (>19× slower) (3/s) 31 | ``` 32 | 33 | Essentially the specific testcase - with no handcrafting involved - needs 30 34 | secs on GRDB, which is state of the art, 2.5 minutes w/ SQLite.swift and 35 | not quite 8 secs with the Lighter API. 36 | 37 | As a chart: 38 | ``` 39 | ┌─────────────────────────────────────────────────────────────────────────────┐ 40 | ├─┐ │ 41 | │S│ Enlighter generated raw SQLite API Bindings ~20% faster │ 42 | ├─┘ │ 43 | ├──┐ │ 44 | │L3│ Lighter, w/ high level API (baseline) Baseline │ 45 | ├──┘ │ 46 | ├─────┐ │ 47 | │GRDB │ with handwritten record mappings ~50% slower │ 48 | ├─────┘ │ 49 | ├─────────────┐ │ 50 | │SQLite.swift │ with handwritten record mappings >3× slower │ 51 | ├─────────────┘ │ 52 | ├───────────────────────┐ │ 53 | │GRDB with Codable │ >6× slower │ 54 | ├───────────────────────┘ │ 55 | ├───────────────────────────────────────────────────────────────────────────┐ │ 56 | │SQLite.swift with Codable >19× slower│ │ 57 | ├───────────────────────────────────────────────────────────────────────────┘ │ 58 | └─────────────────────────────────────────────────────────────────────────────┘ 59 | Time to load the Northwind "Products" table 500 times into a model. 60 | Shorter is faster. 61 | ``` 62 | 63 | *Main takeway*: Never ever use Codable. 64 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | When the plugin doesn't generate. 4 | 5 | ## Overview 6 | 7 | A few things to try: 8 | - Update the Lighter package. 9 | - Make sure Enlighter is added to the build phases as the right plugin. 10 | - Make sure the database and/or SQL files are _within_ the project, 11 | the plugin can only access files within its sandbox. 12 | - If the database isn't available at runtime, make sure it is added as an 13 | Xcode resources and actually gets copied to the application bundle. 14 | 15 | ### Logfile 16 | 17 | Enlighter writes a log to `/tmp/zzdebug.log`, which can be helpful to analyze 18 | issues. 19 | 20 | ### sqlite2swift 21 | 22 | The `sqlite2swift` tool is what gets eventually run, and it can be run 23 | as a standalone tool. 24 | 25 | The arguments: 26 | - 1st argument: The or `-` if there is none. 27 | - 2nd argument: The name of the target the code is generated for, this is 28 | used to resolve configuration options in the file. 29 | - other arguments: The database/SQL input files. 30 | - last argument: The Swift output file. 31 | 32 | ### Debug Lighter itself 33 | 34 | To debug Lighter itself, the git repository can be cloned (e.g. the `develop` 35 | branch). To add that to an own project which already has Lighter as a network 36 | reference, just drag the checked out Lighter folder into the project. 37 | It'll override the network reference and make the package locally editable. 38 | 39 | ### Filing Issues 40 | 41 | Issues with the software can be filed against the public GitHub repository: 42 | [Lighter Issues](https://github.com/Lighter-swift/Lighter/issues). 43 | -------------------------------------------------------------------------------- /Sources/Lighter/Lighter.docc/Who.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Built by @helje5. 4 | 5 | ## ZeeZide 6 | 7 | Lighter is brought to you by 8 | [Helge Heß](https://github.com/helje5/) via [ZeeZide](https://zeezide.de). 9 | We like feedback, GitHub stars, cool contract work, 10 | presumably any form of praise you can think of. 11 | 12 | ## Supporting the Project 13 | 14 | If you want to support the project, consider buying a copy of the 15 | [“Code for SQLite3” application](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010). 16 | A macOS app that does the same like the “`sqlite2swift`” tool, but as an app. 17 | It can be useful to play w/ the Lighter options, but is not required at all. 18 | 19 | Thank you! 20 | 21 | ## Support for Lighter 22 | 23 | Commercial support for Lighter is available from [ZeeZide](https://zeezide.de). 24 | Need help to apply that to your codebase, replace slow CoreData or JSON setups 25 | with superfast SQLite? ZeeZide can help with that 26 | [info@zeezide.de](mailto:info@zeezide.de). 27 | 28 | Issues with the software can be filed against the public GitHub repository: 29 | [Lighter Issues](https://github.com/Lighter-swift/Lighter/issues). 30 | -------------------------------------------------------------------------------- /Sources/Lighter/Operations/SQLDatabaseAsyncFetchOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | // Same like `SQLDatabaseFetchOperations`, async/await variant if available. 7 | 8 | /** 9 | * A mixin protocol to add async/await variants of the "select" 10 | * functions. 11 | * 12 | * Example: 13 | * ```swift 14 | * let persons = try await db.select(from: \.people, \.name, orderBy: \.name) 15 | * ``` 16 | */ 17 | public protocol SQLDatabaseAsyncFetchOperations 18 | : SQLDatabaseAsyncOperations, SQLDatabaseFetchOperations {} 19 | -------------------------------------------------------------------------------- /Sources/Lighter/Operations/SQLDatabaseAsyncOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | import Dispatch 7 | 8 | /** 9 | * An object that can host asynchronous operations. 10 | * 11 | * To accomplish that, it provides the ``asyncDatabaseQueue-5emzh`` 12 | * (which defaults to the `DispatchQueue.global()`). 13 | * 14 | * The actual operations can be found in: 15 | * - ``SQLDatabaseFetchOperations`` 16 | * - ``SQLDatabaseAsyncChangeOperations`` 17 | */ 18 | public protocol SQLDatabaseAsyncOperations: SQLDatabaseOperations, Sendable { 19 | 20 | /** 21 | * The queue that is used to run concurrent async SQL operations. 22 | * 23 | * The maximum SQLite currently supports is multi-reader/single-writer. 24 | * 25 | * Defaults to `DispatchQueue.global()`. 26 | */ 27 | var asyncDatabaseQueue : DispatchQueue { get } 28 | 29 | } 30 | public extension SQLDatabaseAsyncOperations { 31 | 32 | // It might make sense to schedule updates on a single update queue, 33 | // but then updates should generally be run in a transaction anyways. 34 | // So not worth optimizing for. 35 | 36 | /// The default `DispatchQueue` used for asynchronous operations. 37 | @inlinable 38 | var asyncDatabaseQueue : DispatchQueue { 39 | DispatchQueue.global() 40 | } 41 | 42 | #if swift(>=5.5) && canImport(_Concurrency) 43 | /** 44 | * Asynchronously runs the given block in the 45 | * ``SQLDatabaseAsyncOperations/asyncDatabaseQueue-89vi8``. 46 | * 47 | * - Parameters: 48 | * - block: The block to execute in the queue. 49 | * - Returns: The return value of the block. 50 | * - Throws: Rethrows any errors the block throws. 51 | */ 52 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 53 | @inlinable 54 | func runOnDatabaseQueue(block: @Sendable @escaping () throws -> R) async throws -> R 55 | { 56 | return try await withCheckedThrowingContinuation { continuation in 57 | asyncDatabaseQueue.async { 58 | do { continuation.resume(returning : try block()) } 59 | catch { continuation.resume(throwing : error) } 60 | } 61 | } 62 | } 63 | #endif // 5.5 + Concurrency 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Sources/Lighter/Operations/SQLDatabaseFetchOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A mixin protocol to add "select" functions. 8 | * 9 | * Example: 10 | * ```swift 11 | * let names = try db.select(from: \.people, \.name, orderBy: \.name) 12 | * ``` 13 | * 14 | * See also: ``SQLDatabaseAsyncFetchOperations`` for async/await versions. 15 | */ 16 | public protocol SQLDatabaseFetchOperations: SQLDatabaseOperations {} 17 | -------------------------------------------------------------------------------- /Sources/Lighter/Operations/SQLPragmaOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import SQLite3 7 | 8 | public extension SQLDatabaseOperations { 9 | 10 | /** 11 | * Set a SQLite3 pragma to a specified value. 12 | * 13 | * Example: 14 | * ```swift 15 | * try db.set(pragma: "analysis_limit", to: 200) 16 | * ``` 17 | * 18 | * More information: [SQLite PRAGMA](https://www.sqlite.org/pragma.html). 19 | * 20 | * - Parameters: 21 | * - schema: Optional name of schema. 22 | * - name: The name of the pragma, check the SQLite docs for information. 23 | * - value: The value to set the pragma to. 24 | */ 25 | @inlinable 26 | func set(schema: String? = nil, pragma name: String, 27 | to value: SQLiteValueType) 28 | throws 29 | { 30 | let sql = "PRAGMA \(name) = \(value.sqlStringValue);" 31 | try fetch(sql) { _, stop in stop = true } 32 | } 33 | 34 | /** 35 | * Fetch the value of a simple SQLite3 pragma. 36 | * 37 | * Example: 38 | * ```swift 39 | * let limit = try db.get(pragma: "analysis_limit", as: Int.self) 40 | * ``` 41 | * 42 | * More information: [SQLite PRAGMA](https://www.sqlite.org/pragma.html). 43 | * 44 | * - Parameters: 45 | * - schema: Optional name of schema. 46 | * - name: The name of the pragma, check the SQLite docs for information. 47 | * - Returns: The value of the pragma as an Any. 48 | */ 49 | @inlinable 50 | func get(schema: String? = nil, pragma name: String, as type: V.Type) 51 | throws -> V 52 | where V: SQLiteValueType 53 | { 54 | let sql = "PRAGMA \(name);" 55 | var value : V? 56 | try fetch(sql) { statement, stop in 57 | value = try type.init(unsafeSQLite3StatementHandle: statement, column: 0) 58 | stop = true 59 | } 60 | guard let result = value else { throw SQLError(SQLITE_ERROR) } 61 | return result 62 | } 63 | } 64 | 65 | #if swift(>=5.5) && canImport(_Concurrency) 66 | 67 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 68 | public extension SQLDatabaseAsyncOperations { 69 | 70 | /** 71 | * Set a SQLite3 pragma to a specified value. 72 | * 73 | * Example: 74 | * ```swift 75 | * try await db.set(pragma: "analysis_limit", to: 200) 76 | * ``` 77 | * 78 | * More information: [SQLite PRAGMA](https://www.sqlite.org/pragma.html). 79 | * 80 | * - Parameters: 81 | * - schema: Optional name of schema. 82 | * - name: The name of the pragma, check the SQLite docs for information. 83 | * - value: The value to set the pragma to. 84 | */ 85 | @inlinable 86 | func set(schema: String? = nil, pragma name: String, 87 | to value: SQLiteValueType) async throws 88 | { 89 | try await runOnDatabaseQueue { 90 | try set(schema: schema, pragma: name, to: value) 91 | } 92 | } 93 | 94 | /** 95 | * Fetch the value of a simple SQLite3 pragma. 96 | * 97 | * Example: 98 | * ```swift 99 | * let limit = try db.get(pragma: "analysis_limit", as: Int.self) 100 | * ``` 101 | * 102 | * More information: [SQLite PRAGMA](https://www.sqlite.org/pragma.html). 103 | * 104 | * - Parameters: 105 | * - schema: Optional name of schema. 106 | * - name: The name of the pragma, check the SQLite docs for information. 107 | * - type: The type of the simple pragma value, e.g. `Int.self`. 108 | * - Returns: The value of the pragma. 109 | */ 110 | @inlinable 111 | func get(schema: String? = nil, pragma name: String, as type: V.Type) 112 | async throws -> V 113 | where V: SQLiteValueType 114 | { 115 | try await runOnDatabaseQueue { 116 | try get(schema: schema, pragma: name, as: type) 117 | } 118 | } 119 | } 120 | 121 | #endif // swift(>=5.5) && canImport(_Concurrency) 122 | -------------------------------------------------------------------------------- /Sources/Lighter/Operations/SQLRecordFilterOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import SQLite3 7 | 8 | // Filter operations use regular Swift closures for filtering results (as part 9 | // of a SQL `WHERE` expression). 10 | 11 | public extension SQLRecordFetchOperations 12 | where T.Schema: SQLSwiftMatchableSchema 13 | { 14 | 15 | /** 16 | * Fetch filtered records of a view/table w/o any sorting. 17 | * 18 | * Example: 19 | * ``` 20 | * let people = try db.people.filter { $0.id == 2 } 21 | * ``` 22 | * 23 | * - Parameters: 24 | * - limit: An optional fetch limit (defaults to no limit) 25 | * - filter: A Swift closure that receives the associated ``SQLRecord`` 26 | * and can decide whether it should be included in the result. 27 | * - Returns: An array of ``SQLRecord``s matching the type specified. 28 | * - Throws: A ``SQLError`` if a SQLite error occurred. 29 | */ 30 | @inlinable 31 | func filter(limit: Int? = nil, filter: @escaping ( T ) -> Bool) 32 | throws -> [ T ] 33 | where T.Schema: SQLSwiftMatchableSchema 34 | { 35 | try connectionHandler.withConnection(readOnly: true) { db in 36 | try withUnsafePointer(to: filter) { ptr in 37 | // Register/Unregister SQLite function 38 | 39 | guard T.Schema.registerSwiftMatcher(in: db, flags: SQLITE_UTF8, 40 | matcher: ptr) == SQLITE_OK else 41 | { 42 | throw SQLError(db) 43 | } 44 | defer { 45 | _ = T.Schema.unregisterSwiftMatcher(in: db, flags: SQLITE_UTF8) 46 | } 47 | 48 | // Prepare Query 49 | 50 | let sql = T.Schema.matchSelect 51 | 52 | var maybeStmt : OpaquePointer? 53 | guard sqlite3_prepare_v2(db, sql, -1, &maybeStmt, nil) == SQLITE_OK, 54 | let stmt = maybeStmt else 55 | { 56 | assertionFailure("Failed to prepare SQL \(sql)") 57 | throw SQLError(db) 58 | } 59 | defer { sqlite3_finalize(stmt) } 60 | 61 | // Run fetch loop 62 | 63 | var records = [ T ]() 64 | 65 | while true { 66 | let rc = sqlite3_step(stmt) 67 | if rc == SQLITE_DONE { break } 68 | else if rc != SQLITE_ROW { throw SQLError(db) } 69 | 70 | let record = T(stmt, indices: T.Schema.selectColumnIndices) 71 | records.append(record) 72 | } 73 | 74 | return records 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLColumnComparisonPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate that compares two ``SQLColumn``s of a table/view against 8 | * each other. 9 | * 10 | * Example: 11 | * ```swift 12 | * let people = try await db.select(from: \.people, \.id, \.lastname) { 13 | * $0.lastname == $0.firstname 14 | * } 15 | * ``` 16 | */ 17 | public struct SQLColumnComparisonPredicate: SQLPredicate 18 | where L: SQLColumn, 19 | R: SQLColumn, 20 | L.Value == R.Value 21 | { 22 | 23 | public enum ComparisonOperator: String { 24 | 25 | /** 26 | * Check whether the ``SQLColumn`` is the same like the other column 27 | * 28 | * Example: 29 | * ```swift 30 | * $0.personId == $0.managerId 31 | * $0.name == $0.maidenName 32 | * ``` 33 | */ 34 | case equal = "=" 35 | 36 | /** 37 | * Check whether the ``SQLColumn`` is different from the other. 38 | * 39 | * Example: 40 | * ```swift 41 | * $0.personId != $0.managerId 42 | * $0.name != $0.maidenName 43 | * ``` 44 | */ 45 | case notEqual = "!=" 46 | 47 | /** 48 | * Check whether the ``SQLColumn`` is smaller than the other. 49 | * 50 | * Example: 51 | * ```swift 52 | * $0.personId < $0.motherId 53 | * $0.name < $0.lastname 54 | * ``` 55 | */ 56 | case lessThan = "<" 57 | 58 | /** 59 | * Check whether the ``SQLColumn`` is smaller or equal to the other. 60 | * 61 | * Example: 62 | * ```swift 63 | * $0.personId <= $0.motherId 64 | * $0.name <= $0.lastname 65 | * ``` 66 | */ 67 | case lessThanOrEqual = "<=" 68 | 69 | /** 70 | * Check whether the ``SQLColumn`` is greater than the other. 71 | * 72 | * Example: 73 | * ```swift 74 | * $0.personId > $0.motherId 75 | * $0.name > $0.lastname 76 | * ``` 77 | */ 78 | case greaterThan = ">" 79 | 80 | /** 81 | * Check whether the ``SQLColumn`` is greater or equal to the other. 82 | * 83 | * Example: 84 | * ```swift 85 | * $0.personId >= $0.motherId 86 | * $0.name >= $0.lastname 87 | * ``` 88 | */ 89 | case greaterThanOrEqual = ">=" 90 | } 91 | 92 | public let comparator : ComparisonOperator 93 | public let lhs : L 94 | public let rhs : R 95 | 96 | @inlinable 97 | public init(_ lhs: L, _ comparator: ComparisonOperator, _ rhs: R) { 98 | self.comparator = comparator 99 | self.lhs = lhs 100 | self.rhs = rhs 101 | } 102 | 103 | 104 | // MARK: - SQL Generation 105 | 106 | public func generateSQL(into builder: inout SQLBuilder) { 107 | builder.append(builder.sqlString(for: lhs)) 108 | builder.append(" ") 109 | builder.append(comparator.rawValue) 110 | builder.append(" ") 111 | builder.append(builder.sqlString(for: rhs)) 112 | } 113 | } 114 | 115 | #if swift(>=5.5) 116 | extension SQLColumnComparisonPredicate.ComparisonOperator : Sendable {} 117 | #endif 118 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLColumnValueRangePredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate that matches a ``SQLColumn`` of a table/view against 8 | * a range of values. 9 | * 10 | * This generates a SQL `BETWEEN` query. 11 | * 12 | * Example: 13 | * ```swift 14 | * let people = try await db.select(from: \.people, \.id, \.lastname) { 15 | * $0.id.in(1...4) 16 | * || $0.id.in(8..<9) 17 | * && 13...14.contains($0.id) 18 | * } 19 | * ``` 20 | */ 21 | public struct SQLColumnValueRangePredicate: SQLPredicate 22 | where C.Value: Comparable 23 | { 24 | 25 | /// The column to compare the value range against. 26 | public let column : C 27 | 28 | /// The range to compare the column against. 29 | public let values : ClosedRange 30 | 31 | /** 32 | * Setup a new ``SQLColumnValueRangePredicate``. 33 | * 34 | * Examples: 35 | * ```swift 36 | * $0.id.in(1...4) 37 | * $0.id.in(8..<9) 38 | * 13...14.contains($0.id) 39 | * ``` 40 | * 41 | * - Parameters: 42 | * - column: The ``SQLColumn`` to compare. 43 | * - values: The values to compare the column against. 44 | */ 45 | @inlinable 46 | public init(_ column: C, _ values: ClosedRange) { 47 | self.column = column 48 | self.values = values 49 | } 50 | 51 | /** 52 | * Setup a new ``SQLColumnValueRangePredicate``. 53 | * 54 | * Examples: 55 | * ```swift 56 | * $0.personId.in(1...4) 57 | * $0.personId.in(8..<9) 58 | * 13...14.contains($0.personId) 59 | * ``` 60 | * 61 | * - Parameters: 62 | * - column: The ``SQLColumn`` to compare. 63 | * - values: The values to compare the column against. 64 | */ 65 | @inlinable 66 | public init(_ column: C, _ values: Range) 67 | where C.Value: Strideable, C.Value.Stride: SignedInteger 68 | { 69 | self.column = column 70 | self.values = ClosedRange(values) 71 | } 72 | 73 | 74 | // MARK: - SQL Generation 75 | 76 | public func generateSQL(into builder: inout SQLBuilder) { 77 | // in ZeeQL this is done using `is` checks, i.e. not dispatched out to the 78 | // qualifiers. But we want to have it as part of the protocol here. 79 | 80 | builder.append(builder.sqlString(for: column)) 81 | builder.append(" BETWEEN ") 82 | builder.append(builder.sqlString(for: values.lowerBound)) 83 | builder.append(" AND ") 84 | builder.append(builder.sqlString(for: values.upperBound)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLColumnValueSetPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate that matches a ``SQLColumn`` of a table/view against 8 | * a set of values. 9 | * 10 | * This generates a SQL `IN` or `NOT IN` query. 11 | * 12 | * Example: 13 | * ```swift 14 | * let people = try await db.select(from: \.people, \.id, \.lastname) { 15 | * $0.id.in([ 2, 3, 4 ]) 16 | * || $0.id.in(2, 3, 4) 17 | * && $0.id.notIn([ 7, 8 ]) 18 | * && ![ 13, 14 ].contains($0.people) 19 | * && $0.in([ 2, 3 ]) 20 | * } 21 | * ``` 22 | */ 23 | public struct SQLColumnValueSetPredicate: SQLPredicate { 24 | 25 | /// The column to compare the values against. 26 | public let column : C 27 | 28 | /// A set of values to compare the colum against. 29 | public let values : Set 30 | 31 | /// Whether the predicate should be negated (i.e. emit a `NOT IN`). 32 | public let negate : Bool 33 | 34 | /** 35 | * Setup a new ``SQLColumnValueSetPredicate``. 36 | * 37 | * Examples: 38 | * ```swift 39 | * $0.personId.in(values) 40 | * $0.personId.notIn(values) 41 | * ![ 13, 14].contains($0.personId) 42 | * ``` 43 | * 44 | * - Parameters: 45 | * - column: The ``SQLColumn`` to compare. 46 | * - values: The values to compare the column against. 47 | * - negate: Whether the check should negate the query (`NOT IN`). 48 | */ 49 | @inlinable 50 | public init(_ column: C, _ values: Set, negate: Bool = false) { 51 | self.column = column 52 | self.values = values 53 | self.negate = negate 54 | } 55 | 56 | /** 57 | * Setup a new ``SQLColumnValueSetPredicate``. 58 | * 59 | * Examples: 60 | * ```swift 61 | * $0.personId.in(values) 62 | * $0.personId.notIn(values) 63 | * ![ 13, 14].contains($0.personId) 64 | * ``` 65 | * 66 | * - Parameters: 67 | * - column: The ``SQLColumn`` to compare. 68 | * - values: The values to compare the column against. 69 | * - negate: Whether the check should negate the query (`NOT IN`). 70 | */ 71 | @inlinable 72 | public init(_ column: C, _ values: S, negate: Bool = false) 73 | where S.Element == C.Value 74 | { 75 | self.init(column, Set(values), negate: negate) 76 | } 77 | 78 | 79 | // MARK: - SQL Generation 80 | 81 | public func generateSQL(into builder: inout SQLBuilder) { 82 | // in ZeeQL this is done using `is` checks, i.e. not dispatched out to the 83 | // qualifiers. But we want to have it as part of the protocol here. 84 | 85 | let column = builder.sqlString(for: column) 86 | builder.append(column) 87 | builder.append(negate ? " NOT IN ( " : " IN ( ") 88 | var isFirst = true 89 | for value in values { 90 | if isFirst { isFirst = false } else { builder.append(", ") } 91 | builder.append(builder.sqlString(for: value)) 92 | } 93 | builder.append(" )") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLCompoundPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate that joins two predicates by either `AND` or `OR`. 8 | * 9 | * Can be used to construct more complex predicates involving boolean 10 | * operations. 11 | * 12 | * Example: 13 | * ```swift 14 | * $0.lastname.hasPrefix("Da") && $0.age > 10 15 | * ``` 16 | */ 17 | public struct SQLCompoundPredicate: SQLPredicate 18 | where L: SQLPredicate, R: SQLPredicate 19 | { 20 | 21 | /** 22 | * The operator by which the predicates are joined. 23 | */ 24 | public enum Operator: String, Sendable { 25 | /// Both predicates must be true for the whole predicate to be true. 26 | case and = "AND" 27 | /// Either predicate must be true for the whole predicate to be true. 28 | case or = "OR" 29 | } 30 | 31 | /// The operator by which the predicates are combined. 32 | public let operation : Operator 33 | 34 | /// The first predicate. 35 | public let lhs : L 36 | 37 | /// The second predicate. 38 | public let rhs : R 39 | 40 | /** 41 | * Initialize a compound predicate. 42 | * 43 | * It is easier to use the associated operators to create compound 44 | * predicates, i.e. `&&` and `||`. 45 | */ 46 | @inlinable 47 | public init(operation: Operator, lhs: L, rhs: R) { 48 | self.operation = operation 49 | self.lhs = lhs 50 | self.rhs = rhs 51 | } 52 | 53 | 54 | // MARK: - SQL Generation 55 | 56 | public func generateSQL(into builder: inout SQLBuilder) { 57 | builder.append("(") 58 | lhs.generateSQL(into: &builder) 59 | builder.append(") \(operation.rawValue) (") 60 | rhs.generateSQL(into: &builder) 61 | builder.append(")") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLInterpolatedPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate containing raw SQL, that can use string interpolation to generate 8 | * proper values. 9 | * 10 | * Example: 11 | * ```swift 12 | * "\($0.personId) LIKE UPPER(\(name))" 13 | * ``` 14 | */ 15 | public struct SQLInterpolatedPredicate: SQLPredicate, 16 | ExpressibleByStringInterpolation 17 | { 18 | /// The interpolation to use. 19 | @usableFromInline 20 | let interpolation : SQLInterpolation 21 | 22 | @inlinable 23 | public init(stringInterpolation interpolation: SQLInterpolation) { 24 | self.interpolation = interpolation 25 | } 26 | @inlinable 27 | public init(stringLiteral sql: String) { 28 | self.interpolation = SQLInterpolation(verbatim: sql) 29 | } 30 | 31 | 32 | // MARK: - SQL Generation 33 | 34 | public func generateSQL(into builder: inout SQLBuilder) { 35 | interpolation.generateSQL(into: &builder) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLNotPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A predicate that negates the outcome of another. 8 | * 9 | * Example: 10 | * ```swift 11 | * !$0.lastname.contains("uck") 12 | * ``` 13 | */ 14 | public struct SQLNotPredicate: SQLPredicate { 15 | 16 | /// The predicate to be negated. 17 | public let predicate : P 18 | 19 | /** 20 | * Create a new not-predicate using another predicate to be negated. 21 | * 22 | * Note: It is easier to use the `!` operator to negate a predicate. 23 | */ 24 | @inlinable 25 | public init(_ predicate: P) { self.predicate = predicate } 26 | 27 | 28 | // MARK: - SQL Generation 29 | 30 | public func generateSQL(into builder: inout SQLBuilder) { 31 | builder.append("NOT (") 32 | predicate.generateSQL(into: &builder) 33 | builder.append(")") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * Represents a dynamic (but statically typed) predicate that can be rendered 8 | * into a SQL WHERE string. 9 | * 10 | * Common predicates: 11 | * - ``SQLColumnValuePredicate`` (e.g. `$0.id == 10`) 12 | * - ``SQLColumnComparisonPredicate`` (e.g. `$0.id == $0.managerId`) 13 | * - ``SQLNotPredicate`` (e.g. `!($0.id == 10)`) 14 | * - ``SQLCompoundPredicate`` (e.g. `$0.id == 10 && $0.name == "A"`) 15 | * - ``SQLColumnValueRangePredicate`` (e.g. `$0.age.in(49...51)`) 16 | * - ``SQLColumnValueSetPredicate`` (.e.g `$0.type.in([ 'table', 'view' ])`) 17 | * 18 | * Example: 19 | * ``` 20 | * SQLColumnValuePredicate(Person.schema.id, .equal, 10) 21 | * ``` 22 | * results in: 23 | * ``` 24 | * person_id = 10 25 | * ``` 26 | */ 27 | public protocol SQLPredicate: Sendable { 28 | 29 | /** 30 | * Append the SQL representing the predicate to the ``SQLBuilder``. 31 | */ 32 | func generateSQL(into builder: inout SQLBuilder) 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLSortOrder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * The sort order that can be applied in select. Ascending or descending. 8 | */ 9 | public enum SQLSortOrder: String, Sendable { 10 | case ascending = "ASC" 11 | case descending = "DESC" 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Lighter/Predicates/SQLTruePredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * Returns a predicate that always matches. 8 | */ 9 | public struct SQLTruePredicate: SQLPredicate { 10 | 11 | public static let shared = SQLTruePredicate() 12 | 13 | // MARK: - SQL Generation 14 | 15 | static let sql = "1 = 1" 16 | 17 | @inlinable 18 | public init() {} 19 | 20 | public func generateSQL(into builder: inout SQLBuilder) { 21 | builder.append(Self.sql) 22 | } 23 | } 24 | 25 | extension SQLPredicate where Self == SQLTruePredicate { 26 | 27 | @inlinable 28 | static var `true` : SQLTruePredicate { SQLTruePredicate() } 29 | } 30 | 31 | /** 32 | * Returns a predicate that always matches or never. 33 | */ 34 | public struct SQLBoolPredicate: SQLPredicate { 35 | 36 | public static let `true` = Self(true) 37 | public static let `false` = Self(false) 38 | 39 | public let value : Bool 40 | 41 | @inlinable 42 | public init(_ value: Bool) { self.value = value } 43 | 44 | public func generateSQL(into builder: inout SQLBuilder) { 45 | builder.append(value ? "1 = 1" : "1 = 0") 46 | } 47 | } 48 | 49 | extension SQLPredicate where Self == SQLBoolPredicate { 50 | 51 | static var `false` : SQLBoolPredicate { .false } 52 | } 53 | 54 | extension Bool: SQLPredicate { 55 | 56 | public func generateSQL(into builder: inout SQLBuilder) { 57 | if self { 58 | SQLTruePredicate.shared.generateSQL(into: &builder) 59 | } 60 | else { 61 | SQLBoolPredicate.false.generateSQL(into: &builder) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Lighter/Schema/SQLColumn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * Represents a SQL table or view column. 8 | * 9 | * A `Column` is tied to a specific ``SQLRecord`` type (a view or table) and 10 | * has a fixed associated `SQLiteValueType` (`Int`, `String`, `[ UInt8 ]` or 11 | * `Double`). 12 | * 13 | * The `Column` provides keyPath accessors for the associated table object, 14 | * and the external SQL name for query construction.. 15 | * 16 | * Note that SQLite itself (w/o STRICT mode) allows columns to have arbitrary 17 | * types. 18 | */ 19 | public protocol SQLColumn: Hashable, Sendable { 20 | 21 | /// The ``SQLTableRecord`` or ``SQLViewRecord`` of the column. 22 | associatedtype T : SQLRecord 23 | 24 | /// The ``SQLiteValueType`` the column holds. 25 | associatedtype Value : SQLiteValueType & Hashable 26 | 27 | /** 28 | * The external SQL name of the column. 29 | * 30 | * For example `address_id`. 31 | */ 32 | var externalName : String { get } 33 | 34 | /** 35 | * The `defaultValue` is used when a fetch result doesn't contain a 36 | * value for a particular column. 37 | * 38 | * This allows the API to keep non-optional fields, but still be used for 39 | * fragments or records that have not been inserted yet. 40 | */ 41 | var defaultValue : Value { get } 42 | 43 | /** 44 | * A keypath that can be used to access the value of the column in the 45 | * associated ``SQLRecord``. 46 | */ 47 | var keyPath : KeyPath { get } 48 | } 49 | 50 | 51 | // MARK: - Concrete Types 52 | 53 | // Maybe rename to `TableColumn` (w/ r/w keypath) 54 | // ^^ because View's are not generated writable!!! 55 | 56 | /** 57 | * A concrete implementation of the ``SQLColumn`` protocol. 58 | * 59 | * Checkout the ``SQLColumn`` description for more information. 60 | */ 61 | public struct MappedColumn: SQLColumn, 62 | @unchecked Sendable // to workaround `KeyPath` Sendability. 63 | where T: SQLRecord, Value: SQLiteValueType & Hashable 64 | { 65 | // the column itself doesn't need to have identity (though it has in SQLite) 66 | public let externalName : String 67 | public let defaultValue : Value 68 | public let keyPath : KeyPath 69 | 70 | @inlinable 71 | public init(externalName: String, defaultValue: Value, 72 | keyPath: KeyPath) 73 | { 74 | self.externalName = externalName 75 | self.defaultValue = defaultValue 76 | self.keyPath = keyPath 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Lighter/Schema/SQLForeignKeyColumn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A ``SQLColumn`` that is a (single) foreign key targetting a different column 8 | * in another table. 9 | */ 10 | public protocol SQLForeignKeyColumn: SQLColumn, Sendable { 11 | 12 | /// The type of the ``SQLColumn`` the foreign key is targetting. 13 | associatedtype DestinationColumn : SQLColumn 14 | 15 | /// The type of the ``SQLTableRecord`` the foreign key is targetting. 16 | typealias Destination = DestinationColumn.T 17 | 18 | /// The destination ``SQLColumn`` the foreign key is targetting. 19 | var destinationColumn : DestinationColumn { get } 20 | } 21 | 22 | 23 | /** 24 | * A concrete implementation of the ``SQLForeignKeyColumn`` protocol. 25 | * 26 | * Checkout the ``SQLForeignKeyColumn`` description for more information. 27 | */ 28 | public struct MappedForeignKey 29 | : SQLForeignKeyColumn, 30 | @unchecked Sendable // to workaround `KeyPath` Sendability. 31 | where T: SQLRecord, Value: SQLiteValueType & Hashable, 32 | DestinationColumn: SQLColumn 33 | { 34 | // the column itself doesn't need to have identity (though it has in SQLite) 35 | public let externalName : String 36 | public let defaultValue : Value 37 | public let keyPath : KeyPath 38 | 39 | @inlinable 40 | public var destinationColumn : DestinationColumn { _destinationColumn() } 41 | 42 | @usableFromInline 43 | internal let _destinationColumn : () -> DestinationColumn 44 | 45 | @inlinable 46 | public init(externalName: String, defaultValue: Value, 47 | keyPath: KeyPath, 48 | destinationColumn: 49 | @Sendable @escaping @autoclosure () -> DestinationColumn) 50 | { 51 | self.externalName = externalName 52 | self.defaultValue = defaultValue 53 | self.keyPath = keyPath 54 | self._destinationColumn = destinationColumn 55 | } 56 | 57 | @inlinable 58 | public static func ==(lhs: Self, rhs: Self) -> Bool { 59 | guard lhs.externalName == rhs.externalName else { return false } 60 | guard lhs.defaultValue == rhs.defaultValue else { return false } 61 | guard lhs.destinationColumn == rhs.destinationColumn else { return false } 62 | guard lhs.keyPath == rhs.keyPath else { return false } 63 | return true 64 | } 65 | 66 | @inlinable 67 | public func hash(into hasher: inout Hasher) { 68 | externalName .hash(into: &hasher) 69 | destinationColumn.hash(into: &hasher) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Lighter/Schema/SQLSwiftMatchableSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * An entity schema which supports selects that filter using a Swift closure 8 | * (vs a SQL `WHERE` condition). 9 | */ 10 | public protocol SQLSwiftMatchableSchema: SQLEntitySchema { 11 | 12 | /// The query used for filter operations. 13 | /// 14 | /// It starts with the ``SQLEntitySchema/select`` and has the Swift matcher 15 | /// in a `WHERE` clause. 16 | static var matchSelect : String { get } 17 | 18 | /** 19 | * Register the Swift matcher closure used to filter SQL results. 20 | * This is called before filter queries are issued. 21 | * 22 | * - Parameters: 23 | * - db: A SQLite3 database handle. 24 | * - flags: Flags to pass over to the function registration. 25 | * - matcher: A raw pointer to the Swift closure used for evaluation. 26 | * - Returns: The SQLite3 error code if something failed. 27 | */ 28 | static func registerSwiftMatcher (in db: OpaquePointer!, flags: Int32, 29 | matcher: UnsafeRawPointer) 30 | -> Int32 31 | /** 32 | * Unregister the Swift matcher closure used to filter SQL results. 33 | * This is called after filter queries had been issued. 34 | * 35 | * - Parameters: 36 | * - db: A SQLite3 database handle. 37 | * - flags: Flags to pass over to the function registration. 38 | * - Returns: The SQLite3 error code if something failed. 39 | */ 40 | static func unregisterSwiftMatcher(in db: OpaquePointer!, flags: Int32) 41 | -> Int32 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Lighter/Schema/SQLValueChanges.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * Types conforming to this protocol can diff themselves against each other. 8 | */ 9 | public protocol SQLValueChanges { 10 | 11 | #if swift(>=5.7) 12 | /// Report the changes to the other record. 13 | func changes(from other: Self) -> [ any SQLColumnValueChange ] 14 | #endif 15 | 16 | /// Report the changes to the other record. 17 | @inlinable 18 | func anyChanges(from other: Self) -> [ String : Any ] 19 | } 20 | 21 | 22 | /** 23 | * Represents a value change in a ``SQLColumn`` that is attached to a 24 | * ``SQLTableRecord``. 25 | * 26 | * The change has access to the column itself, and to the old and new values. 27 | */ 28 | public protocol SQLColumnValueChange { 29 | 30 | /// The type of the ``SQLColumn`` that was changed. 31 | associatedtype C: SQLColumn 32 | 33 | /// The ``SQLColumn`` that was changed. 34 | var column : C { get } 35 | 36 | /// The old value of the column. 37 | var oldValue : C.Value { get } 38 | 39 | /// The new value of the column. 40 | var newValue : C.Value { get } 41 | } 42 | 43 | /** 44 | * A concrete implementation of the ``SQLColumnValueChange`` protocol. 45 | * 46 | * Checkout the ``SQLColumnValueChange`` description for more information. 47 | */ 48 | public struct MappedColumnValueChange: SQLColumnValueChange 49 | where C: SQLColumn, C.T: SQLTableRecord 50 | { 51 | // the column itself doesn't need to have identity 52 | public let column : C 53 | public let oldValue : C.Value 54 | public let newValue : C.Value 55 | } 56 | 57 | #if swift(>=5.7) // for `any` 58 | /** 59 | * A helper object for record diffing. 60 | */ 61 | @dynamicMemberLookup 62 | public struct SQLRecordDiffingState { 63 | 64 | @inlinable 65 | var schema : T.Schema { T.schema } 66 | 67 | @usableFromInline 68 | var changes = [ any SQLColumnValueChange ]() 69 | 70 | /** 71 | * The dynamic subscript allows the user to write `$0.personId` in: 72 | * ``` 73 | * $0.addIfChanged($0.personId, ...) // <== 74 | * ``` 75 | * it resolves to the ``SQLColumn`` in the ``SQLRecord``'s schema. 76 | */ 77 | @inlinable 78 | public subscript(dynamicMember path: KeyPath) -> C 79 | where C: SQLColumn 80 | { 81 | T.schema[keyPath: path] 82 | } 83 | 84 | public mutating func addIfChanged(_ column: C, old: C.Value, new: C.Value) 85 | where C: SQLColumn, C.T: SQLTableRecord 86 | { 87 | guard old != new else { return } 88 | changes.append(MappedColumnValueChange( 89 | column: column, oldValue: old, newValue: new) 90 | ) 91 | } 92 | } 93 | 94 | public extension SQLTableRecord { 95 | 96 | // Later: We need a boolean variant for fast compares (i.e. didChange(from:)) 97 | 98 | /** 99 | * This can be used by model classes to diff two models. 100 | * 101 | * Example: 102 | * ```swift 103 | * public extension Person { 104 | * 105 | * func changes(from other: Self) -> [ any SQLColumnValueChange ] { 106 | * calculateChanges { 107 | * $0.addIfChanged($0.personId, old: other.personId, new: personId) 108 | * $0.addIfChanged($0.firstname, old: other.firstname, new: firstname) 109 | * $0.addIfChanged($0.lastname, old: other.lastname, new: lastname) 110 | * } 111 | * } 112 | * } 113 | * ``` 114 | */ 115 | func calculateChanges 116 | (using builder: (inout SQLRecordDiffingState) -> Void) 117 | -> [ any SQLColumnValueChange ] 118 | { 119 | var changes = SQLRecordDiffingState() 120 | builder(&changes) 121 | return changes.changes 122 | } 123 | } 124 | #endif // Swift 5.7 125 | -------------------------------------------------------------------------------- /Sources/Lighter/Transactions/SQLChangeTransaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A SQL transaction allows the user to run multiple SQL operations 8 | * as a single, atomic unit. 9 | * 10 | * A SQLChangeTransaction allows modifications to the database (updates, 11 | * inserts and deletes), as provided by ``SQLDatabaseChangeOperations``. 12 | * 13 | * Transactions can be started on database objects, like: 14 | * ```swift 15 | * try await db.transaction { tx in 16 | * let firstPerson = try tx.people.find(1) 17 | * var secondPerson = try tx.people.find(2) 18 | * 19 | * secondPerson.lastName = "New Name" 20 | * try tx.update(secondPerson) 21 | * } 22 | * ``` 23 | * 24 | * Note: Within a transaction async calls are not allowed. 25 | */ 26 | public final class SQLChangeTransaction 27 | : SQLTransaction, SQLDatabaseChangeOperations 28 | { 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Lighter/Transactions/SQLTransaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * A SQL transaction allows the user to run multiple SQL operations 8 | * as a single, atomic unit. 9 | * 10 | * Transactions are rolled back if anything within throws an error. 11 | * 12 | * Transactions can be started on database objects, like: 13 | * ```swift 14 | * try await db.transaction { tx in 15 | * var person = try tx.people.find(1) 16 | * person.name = "Spitz" 17 | * try tx.update(person) 18 | * } 19 | * ``` 20 | * If the transaction is read-only, the optimized version can be used 21 | * (it also ensures that modifications are not triggered accidentially by 22 | * statically not-providing the relevant methods): 23 | * ```swift 24 | * try await db.readTransaction { tx in 25 | * let firstPerson = try tx.people.find(1) 26 | * let secondPerson = try tx.people.find(2) 27 | * } 28 | * ``` 29 | * 30 | * Note: Within a transaction async calls are not allowed. 31 | * This is intentional. A transaction can lock database objects, 32 | * often the whole database. An async call can take an unspecified amount 33 | * of time and there are no guarantees when it completes as it is 34 | * cooperatively scheduled. This should leave a DB tx hanging. 35 | * 36 | * #### Performance 37 | * 38 | * A transaction has a fixed SQLite3 database assigned, which is why it is 39 | * also good when the best possible performance is required. 40 | * 41 | * It also avoids thread hops when async/await is being used. I.e. this: 42 | * ```swift 43 | * try await db.readTransaction { tx in 44 | * let firstPerson = try tx.people.find(1) 45 | * let secondPerson = try tx.people.find(2) 46 | * } 47 | * ``` 48 | * is better than: 49 | * ```swift 50 | * let firstPerson = try await db.people.find(1) 51 | * let secondPerson = try await db.people.find(2) 52 | * ``` 53 | * Though if concurrency is really wanted, this can be appropriate too: 54 | * ```swift 55 | * async let firstPerson = db.people.find(1) 56 | * async let secondPerson = db.people.find(2) 57 | * try await doSth(with: firstPerson, and: secondPerson) 58 | * ``` 59 | * (be careful w/ thrashing though) 60 | */ 61 | @dynamicMemberLookup 62 | public class SQLTransaction: SQLDatabaseFetchOperations { 63 | 64 | /// The ``SQLDatabase`` the transaction is running against. 65 | public let database : DB 66 | 67 | /// The ``SQLRecordFetchOperations/recordTypes`` associated w/ the database. 68 | @inlinable 69 | public static var recordTypes : DB.RecordTypes { DB.recordTypes } 70 | 71 | /// The ``SQLConnectionHandler`` used for the transaction. This is only valid 72 | /// for one specific transaction. 73 | public let connectionHandler : SQLConnectionHandler 74 | 75 | /** 76 | * Initialize a new ``SQLTransaction``. Do not use directly. 77 | * 78 | * Instead call the ``SQLDatabase/transaction(mode:execute:)-kgor`` function 79 | * and companions. 80 | * 81 | * - Parameters: 82 | * - database: The ``SQLDatabase`` the transaction is running against. 83 | * - handle: The SQLite3 database handle that was assigned to the 84 | * transaction by the database. 85 | */ 86 | @inlinable 87 | public init(_ database: DB, handle: OpaquePointer) { 88 | self.database = database 89 | self.connectionHandler = 90 | .unsafeReuse(handle, url: database.connectionHandler.url) 91 | } 92 | 93 | // Later: request manual rollback/commit (can already be done by throwing). 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Lighter/Transactions/SQLTransactionAsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | // The async/await variants of the SQLDatabase transaction operations, 7 | // if Swift concurrency is available. 8 | 9 | #if swift(>=5.5) && canImport(_Concurrency) 10 | 11 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 12 | public extension SQLDatabase where Self: SQLDatabaseAsyncOperations { 13 | 14 | /** 15 | * A SQL transaction allows the user to run multiple SQL operations 16 | * as a single, atomic unit. 17 | * 18 | * Transactions can be started on database objects, like: 19 | * ```swift 20 | * try await db.transaction { tx in 21 | * var person = try tx.people.find(1) 22 | * person.name = "Spitz" 23 | * try tx.update(person) 24 | * } 25 | * ``` 26 | * If the transaction is read-only (does no modifications to the database), 27 | * the ``SQLDatabase/readTransaction(execute:)-8mbsj`` should be used. 28 | * SQLite only supports one writer per database, using this method will 29 | * acquire such a lock by default. Using `readTransaction` avoids that 30 | * (multiple readers are allowed, in particular if the DB is set to the WAL 31 | * mode). 32 | * 33 | * Note: Within a transaction async calls are not allowed (as they can 34 | * block the transaction, and with it the database, for a unforseeable 35 | * time). 36 | * 37 | * - Parameters: 38 | * - mode: The mode defaults to ``SQLTransactionType/immediate``, which 39 | * opens/waits for the database lock right away. 40 | * It can be set to ``SQLTransactionType/deferred`` to start with 41 | * a read-lock, but note that upgrades on locked databases will 42 | * fail w/ `SQLITE_BUSY` immediately. 43 | * - execute: The code which is executed within the transaction 44 | * - Returns: The result of the `execute` closure if the transaction got 45 | * committed successfully. 46 | */ 47 | @inlinable 48 | @discardableResult 49 | func transaction( 50 | mode : SQLTransactionType = .default, 51 | execute : @Sendable @escaping ( SQLChangeTransaction ) throws -> R 52 | ) async throws -> R 53 | { 54 | try await runOnDatabaseQueue { 55 | try transaction(mode: mode, execute: execute) 56 | } 57 | } 58 | 59 | /** 60 | * A read-only SQL transaction allows the user to run multiple SQL operations 61 | * efficiently. 62 | * 63 | * To modify records, ``SQLDatabase/transaction(mode:execute:)-kgor`` must 64 | * be used. 65 | * 66 | * ```swift 67 | * try await db.readTransaction { tx in 68 | * let person1 = try tx.people.find(1) 69 | * let person2 = try tx.people.find(2) 70 | * } 71 | * ``` 72 | * 73 | * A read only transaction is always rolled back and never committed. 74 | * A read transaction is always opened in ``SQLTransactionType/deferred`` 75 | * mode. 76 | * 77 | * Note: Within a transaction async calls are not allowed. 78 | * 79 | * - Parameters: 80 | * - execute: The code which is executed within the transaction 81 | * - Returns: The result of the `execute` closure if the transaction got 82 | * rolled back successfully. 83 | */ 84 | @inlinable 85 | @discardableResult 86 | func readTransaction(execute: @Sendable @escaping 87 | (SQLTransaction) throws -> R) 88 | async throws -> R 89 | { 90 | try await runOnDatabaseQueue { 91 | try readTransaction(execute: execute) 92 | } 93 | } 94 | } 95 | 96 | #endif // 5.5 + Concurrency 97 | -------------------------------------------------------------------------------- /Sources/Lighter/Transactions/SQLTransactionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * The transaction type defines whether a transaction needs write access 8 | * and in non-WAL mode, whether a transaction is exclusive (i.e. forbids 9 | * concurrent reads). 10 | * 11 | * The default is ``immediate``, which directly acquires the database write 12 | * lock. 13 | * It is preferred over ``deferred``, because transaction upgrades will 14 | * immediately fail w/ `SQLITE_BUSY` if the database lock is in use. 15 | * While an immediate transaction will wait to acquire the lock. 16 | */ 17 | public enum SQLTransactionType: String, Sendable { 18 | 19 | /// Start a read transaction on the first SELECT and upgrade to a write 20 | /// transaction on the first modification. 21 | /// Careful: When transactions are upgraded by writes and the database is 22 | /// locked already, a `SQLITE_BUSY` error will be issued immediately 23 | /// (i.e. it won't wait for the lock becoming available). 24 | case deferred = "DEFERRED" 25 | 26 | /// Immediatly start a writable transaction. This will acquire (and possibly 27 | /// wait) for the database write lock. 28 | case immediate = "IMMEDIATE" 29 | 30 | /// The same like ``immediate`` in WAL mode, but protects against concurrent 31 | /// reads in others. 32 | case exclusive = "EXCLUSIVE" 33 | 34 | @inlinable 35 | public static var `default` : SQLTransactionType { .immediate } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Lighter/Utilities/LighterError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2023 ZeeZide GmbH. 4 | // 5 | 6 | import struct Foundation.URL 7 | 8 | /** 9 | * An error that is thrown by Lighter database operations. 10 | * 11 | * This carries a higher level error type as well as the SQLite3 12 | * error code and message. 13 | */ 14 | public struct LighterError: Swift.Error, Sendable { 15 | 16 | /// The higher level error type. 17 | public let type : ErrorType 18 | 19 | /// The SQLite3 error code. 20 | public let code : Int32 21 | /// The SQLite3 error message. 22 | public let message : String? 23 | 24 | @inlinable 25 | public init(_ type: ErrorType, 26 | _ code: Int32, _ message: UnsafePointer? = nil) 27 | { 28 | self.type = type 29 | self.code = code 30 | self.message = message.flatMap(String.init(cString:)) 31 | } 32 | 33 | /// The kind of the error that happened within Lighter. 34 | public enum ErrorType: Hashable, Sendable { 35 | 36 | // Those are Sendable, they are the record types internally. 37 | case insertFailed(record: any SQLInsertableRecord) 38 | case updateFailed(record: any SQLUpdatableRecord) 39 | case deleteFailed(record: any SQLDeletableRecord) 40 | 41 | case couldNotOpenDatabase(URL) 42 | 43 | case couldNotBeginTransaction 44 | case couldNotRollbackTransaction 45 | case couldNotCommitTransaction 46 | 47 | case couldNotFindRelationshipTarget 48 | 49 | @inlinable 50 | public static func ==(lhs: Self, rhs: Self) -> Bool { 51 | func isEqual(lhs: T, rhs: any Equatable) -> Bool { 52 | guard let rhs = rhs as? T else { return false } 53 | return lhs == rhs 54 | } 55 | 56 | switch ( lhs, rhs ) { 57 | case ( .insertFailed(let lhs), .insertFailed(let rhs)): 58 | return isEqual(lhs: lhs, rhs: rhs) 59 | case ( .updateFailed(let lhs), .updateFailed(let rhs)): 60 | return isEqual(lhs: lhs, rhs: rhs) 61 | case ( .deleteFailed(let lhs), .deleteFailed(let rhs)): 62 | return isEqual(lhs: lhs, rhs: rhs) 63 | 64 | case ( .couldNotOpenDatabase(let lhs), .couldNotOpenDatabase(let rhs)): 65 | return lhs == rhs 66 | 67 | case ( .couldNotBeginTransaction, .couldNotBeginTransaction ): 68 | return true 69 | case ( .couldNotRollbackTransaction, .couldNotRollbackTransaction ): 70 | return true 71 | case ( .couldNotCommitTransaction, .couldNotCommitTransaction ): 72 | return true 73 | case ( .couldNotFindRelationshipTarget, 74 | .couldNotFindRelationshipTarget ): 75 | return true 76 | 77 | default: 78 | return false 79 | } 80 | } 81 | 82 | public func hash(into hasher: inout Hasher) { 83 | switch self { 84 | case .insertFailed(let record) : record.hash(into: &hasher) 85 | case .updateFailed(let record) : record.hash(into: &hasher) 86 | case .deleteFailed(let record) : record.hash(into: &hasher) 87 | 88 | case .couldNotOpenDatabase(let url): url.hash(into: &hasher) 89 | 90 | // TBD: 91 | case .couldNotBeginTransaction : 1.hash(into: &hasher) 92 | case .couldNotRollbackTransaction : 2.hash(into: &hasher) 93 | case .couldNotCommitTransaction : 3.hash(into: &hasher) 94 | case .couldNotFindRelationshipTarget : 4.hash(into: &hasher) 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Lighter/Utilities/OptionalCString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | public extension Optional where Wrapped == String { 7 | 8 | /** 9 | * Call a closure with the C string representation of the optional String, 10 | * or `nil` if the optional is `.none`. 11 | * 12 | * This is a helper to deal with optional String values. 13 | */ 14 | @inlinable 15 | func withCString(_ body: (UnsafePointer?) throws -> R) 16 | rethrows -> R 17 | { 18 | // try breaks this trick: try self?.withCString(body) ?? body(nil) 19 | if let v = self { return try v.withCString(body) } 20 | else { return try body(nil) } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Lighter/Utilities/SQLError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022-2024 ZeeZide GmbH. 4 | // 5 | 6 | import func SQLite3.sqlite3_errcode 7 | import func SQLite3.sqlite3_errmsg 8 | 9 | /** 10 | * A raw SQLite3 error. 11 | * 12 | * Encapsulates the SQLite3 error code and message in a throwable Swift error. 13 | * 14 | * It can be initialized from a raw SQLite3 database handle, e.g.: 15 | * ```swift 16 | * let rc = sqlite3_exec...(db) 17 | * guard rc == SQLITE3_OK else { throw SQLError(db) } 18 | * ``` 19 | */ 20 | public struct SQLError: Swift.Error, Equatable { 21 | 22 | /// The SQLite3 error code. 23 | public let code : Int32 24 | /// The SQLite3 error message. 25 | public let message : String? 26 | 27 | /** 28 | * Create a new ``SQLError`` from the SQLite3 error code and message. 29 | * 30 | * - Parameters: 31 | * - code: The SQLite3 error code. 32 | * - message: The SQLite3 error message. 33 | */ 34 | @inlinable 35 | public init(_ code: Int32, _ message: UnsafePointer? = nil) { 36 | self.code = code 37 | self.message = message.flatMap(String.init(cString:)) 38 | } 39 | 40 | /** 41 | * Create a new ``SQLError`` from the error contained in a SQLite3 database 42 | * handle. 43 | * 44 | * - Parameters: 45 | * - db: A SQLite3 database handle. 46 | */ 47 | @inlinable 48 | public init(_ db: OpaquePointer!) { 49 | self.code = sqlite3_errcode(db) 50 | self.message = sqlite3_errmsg(db).flatMap(String.init(cString:)) 51 | } 52 | } 53 | #if swift(>=5.5) 54 | extension SQLError: Sendable {} 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/Lighter/Utilities/SendableKeyPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | #if compiler(<6) // Looks like the KeyPath is Sendable in Swift 6. 7 | // Ugh. But how else? Swift 6 maybe. 8 | extension KeyPath: @unchecked Sendable 9 | where Root: Sendable, Value: Sendable {} 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SQLite3-Linux/README.md: -------------------------------------------------------------------------------- 1 |

SQLite3 Module for Linux and Others 2 | 4 |

5 | 6 | On Darwin platforms (macOS/iOS/..), SQLite3 is pre-bundled under that name. To 7 | support Linux, we embed this system module. 8 | 9 | Usually a module like this is called "CSQLite3", as provided by 10 | ZeeQL: [CSQLite3](https://github.com/ZeeQL/CSQLite3). Using `SQLite3` here for 11 | Darwin compatibility. 12 | 13 | The `libsqlite3-dev` package needs to be installed on the Linux system for 14 | compilation. 15 | 16 | 17 | ### Who 18 | 19 | Lighter is brought to you by 20 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 21 | We like feedback, GitHub stars, cool contract work, 22 | presumably any form of praise you can think of. 23 | -------------------------------------------------------------------------------- /Sources/SQLite3-Linux/module.modulemap: -------------------------------------------------------------------------------- 1 | module SQLite3 [system] { 2 | header "shim.h" 3 | link "sqlite3" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/SQLite3-Linux/shim.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /Sources/SQLite3Schema/README.md: -------------------------------------------------------------------------------- 1 |

SQLite3Schema 2 | 4 |

5 | 6 | Small library to fetch the SQLite3 database catalog. 7 | 8 | Grab the schema of a SQLite3 database. That is, tables, views, foreign keys 9 | and more. 10 | 11 | Has no other dependencies, directly works w/ the `SQLite3` module that is 12 | provided on macOS/iOS/etc. A SQLite3 stub for Linux is provided as well. 13 | 14 | 15 | ### Who 16 | 17 | Lighter is brought to you by 18 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 19 | We like feedback, GitHub stars, cool contract work, 20 | presumably any form of praise you can think of. 21 | -------------------------------------------------------------------------------- /Tests/CodeGenASTTests/BuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | 10 | final class BuilderTests: XCTestCase { 11 | 12 | func testFunctionDeclaration() { 13 | let f = Fixtures.makeSelectDeclaration() 14 | XCTAssertEqual(f.name, "select") 15 | XCTAssertEqual(f.genericParameterNames.count, 3) 16 | XCTAssertEqual(f.parameters.count, 5) 17 | XCTAssertTrue (f.throws) 18 | XCTAssertEqual(f.genericConstraints.count, 4) 19 | } 20 | 21 | func testFunctionDefinition() { 22 | let f = Fixtures.makeSelectDefinition() 23 | XCTAssertTrue(f.declaration.throws) 24 | } 25 | 26 | func testUnitWithExtension() { 27 | let f : FunctionDefinition = Fixtures.makeSelectDefinition() 28 | let ext = Extension(extendedType: .name("SQLDatabaseFetchOperations"), 29 | functions: [ f ]) 30 | let unit = 31 | CompilationUnit(name: "GeneratedSelectOperations", extensions: [ ext ]) 32 | 33 | XCTAssertEqual(unit.extensions.count, 1) 34 | XCTAssertEqual(unit.extensions.first?.functions.count, 1) 35 | XCTAssertEqual(ext.public, true) 36 | } 37 | 38 | func testExtensionWithNestedStruct() { 39 | let s = TypeDefinition(kind: .struct, name: "RecordTypes") 40 | let ext = Extension(extendedType: .name("SQLDatabaseFetchOperations"), 41 | typeDefinitions: [ s ]) 42 | let unit = 43 | CompilationUnit(name: "GeneratedSelectOperations", extensions: [ ext ]) 44 | 45 | XCTAssertEqual(unit.extensions.count, 1) 46 | XCTAssertEqual(unit.extensions.first?.functions.count, 0) 47 | XCTAssertEqual(ext.public, true) 48 | XCTAssertEqual(s.public, true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/ContactsDatabaseTests/ContactsDatabaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import Lighter 9 | 10 | final class ContactsDatabaseTests: XCTestCase { 11 | 12 | // This doesn't contain any actual tests. It is a helper test bundle to 13 | // try out the code generator. 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ContactsDatabaseTests/contacts-create.sql: -------------------------------------------------------------------------------- 1 | /** 2 | * ZeeQL test schema 3 | * 4 | * Copyright © 2017-2024 ZeeZide GmbH. All rights reserved. 5 | */ 6 | 7 | CREATE TABLE person ( 8 | person_id INTEGER PRIMARY KEY NOT NULL, 9 | 10 | firstname VARCHAR NULL, 11 | lastname VARCHAR NOT NULL 12 | ); 13 | 14 | CREATE TABLE address ( 15 | address_id INTEGER PRIMARY KEY NOT NULL, 16 | 17 | street VARCHAR NULL, 18 | city VARCHAR NULL DEFAULT 'Magdeburg', 19 | state VARCHAR NULL, 20 | country VARCHAR NULL, 21 | 22 | age INTEGER DEFAULT NULL, 23 | answer INTEGER DEFAULT 42, 24 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 25 | 26 | 27 | person_id INTEGER, 28 | FOREIGN KEY(person_id) REFERENCES person(person_id) 29 | ON DELETE CASCADE 30 | DEFERRABLE 31 | ); 32 | -------------------------------------------------------------------------------- /Tests/EntityGenTests/ASTRecordBindGenerationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class ASTRecordBindGenerationTests: XCTestCase { 12 | 13 | func testPersonStatementBindWithLocalHelpers() throws { 14 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 15 | let options = Fancifier.Options() 16 | let fancifier = Fancifier(options: options) 17 | fancifier.fancifyDatabaseInfo(dbInfo) 18 | //print("Fancified:", dbInfo) 19 | 20 | let gen = EnlighterASTGenerator( 21 | database: dbInfo, filename: "Contacts.swift" 22 | ) 23 | gen.options.optionalHelpersInDatabase = false 24 | let s = gen.generateRecordStatementBind(for: try XCTUnwrap(dbInfo["Person"])) 25 | 26 | let source : String = { 27 | let builder = CodeGenerator() 28 | builder.generateFunctionDefinition(s) 29 | return builder.source 30 | }() 31 | //print("GOT:\n-----\n\(source)\n-----") 32 | 33 | XCTAssertTrue(source.contains( 34 | "sqlite3_bind_int64(statement, indices.idx_id")) 35 | XCTAssertTrue(source.contains("return try withOptCString(firstname)")) 36 | XCTAssertTrue(source.contains( 37 | "func withOptCString(_ s: String?, _ body")) 38 | XCTAssertTrue(source.contains("return try execute()")) 39 | } 40 | 41 | func testPersonStatementBindWithDBHelpers() throws { 42 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 43 | let options = Fancifier.Options() 44 | let fancifier = Fancifier(options: options) 45 | fancifier.fancifyDatabaseInfo(dbInfo) 46 | //print("Fancified:", dbInfo) 47 | 48 | let gen = EnlighterASTGenerator( 49 | database: dbInfo, filename: "Contacts.swift" 50 | ) 51 | gen.options.optionalHelpersInDatabase = true 52 | let s = gen.generateRecordStatementBind(for: try XCTUnwrap(dbInfo["Person"])) 53 | 54 | let source : String = { 55 | let builder = CodeGenerator() 56 | builder.generateFunctionDefinition(s) 57 | return builder.source 58 | }() 59 | // print("GOT:\n-----\n\(source)\n-----") 60 | 61 | XCTAssertTrue(source.contains( 62 | "sqlite3_bind_int64(statement, indices.idx_id")) 63 | XCTAssertFalse(source.contains("return try withOptCString(firstname)")) 64 | XCTAssertFalse(source.contains( 65 | "func withOptCString(_ s: String?, _ body")) 66 | XCTAssertTrue(source.contains("return try execute()")) 67 | XCTAssertTrue(source.contains("try TestDB.withOptCString")) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/EntityGenTests/ASTRecordRelshipGenerationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class ASTRecordRelshipGenerationTests: XCTestCase { 12 | 13 | func testFindPersonForAddress() throws { 14 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 15 | let options = Fancifier.Options() 16 | let fancifier = Fancifier(options: options) 17 | fancifier.fancifyDatabaseInfo(dbInfo) 18 | //print("Fancified:", dbInfo) 19 | 20 | let gen = EnlighterASTGenerator( 21 | database: dbInfo, filename: "Contacts.swift" 22 | ) 23 | 24 | let Address = try XCTUnwrap(dbInfo["Address"]) 25 | let AddressToPerson = try XCTUnwrap(Address[toOne: "Person"]) 26 | let s = gen.generateFind(for: Address, relationship: AddressToPerson) 27 | 28 | let source : String = { 29 | let builder = CodeGenerator() 30 | builder.generateFunctionDefinition(s) 31 | return builder.source 32 | }() 33 | // print("GOT:\n-----\n\(source)\n-----") 34 | 35 | XCTAssertTrue(source.contains( 36 | "Fetch the ``Person`` record related to a ``Address``")) 37 | XCTAssertTrue(source.contains( 38 | "relatedRecord = try db.people.find(for: sourceRecord")) 39 | XCTAssertTrue(source.contains( 40 | "public func find(`for` record: Address) throws -> Person?")) 41 | XCTAssertTrue(source.contains( 42 | "try operations[dynamicMember: \\.addresses]")) 43 | XCTAssertTrue(source.contains( 44 | "findTarget(for: \\.personId, in: record)")) 45 | } 46 | 47 | 48 | func testFindAddressesForPerson() throws { 49 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 50 | let options = Fancifier.Options() 51 | let fancifier = Fancifier(options: options) 52 | fancifier.fancifyDatabaseInfo(dbInfo) 53 | //print("Fancified:", dbInfo) 54 | 55 | let gen = EnlighterASTGenerator( 56 | database: dbInfo, filename: "Contacts.swift" 57 | ) 58 | 59 | let Person = try XCTUnwrap(dbInfo["Person"]) 60 | let PersonToAddresses = try XCTUnwrap(Person[toMany: "Addresses"]) 61 | let s = gen.generateFetch(for: Person, relationship: PersonToAddresses, 62 | async: true) 63 | 64 | let source : String = { 65 | let builder = CodeGenerator() 66 | builder.generateFunctionDefinition(s) 67 | return builder.source 68 | }() 69 | //print("GOT:\n-----\n\(source)\n-----") 70 | 71 | XCTAssertTrue(source.contains( 72 | "Fetches the ``Address`` records related to a ``Person``")) 73 | XCTAssertTrue(source.contains( 74 | "let relatedRecords = try await db.addresses.fetch(for: record")) 75 | XCTAssertTrue(source.contains( 76 | "public func fetch(`for` record: Person, limit: Int?")) 77 | XCTAssertTrue(source.contains( 78 | "async throws -> [ Address ]")) 79 | XCTAssertTrue(source.contains( 80 | "try await fetch(for: \\.personId, in: record, limit: limit)")) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/EntityGenTests/ASTRecordStructGenerationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class ASTRecordStructGenerationTests: XCTestCase { 12 | 13 | func testPersonStruct() throws { 14 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 15 | let options = Fancifier.Options() 16 | let fancifier = Fancifier(options: options) 17 | fancifier.fancifyDatabaseInfo(dbInfo) 18 | //print("Fancified:", dbInfo) 19 | 20 | let gen = EnlighterASTGenerator( 21 | database: dbInfo, filename: "Contacts.swift" 22 | ) 23 | let s = gen.generateRecordStructure(for: try XCTUnwrap(dbInfo["Person"])) 24 | 25 | let source : String = { 26 | let builder = CodeGenerator() 27 | builder.generateStruct(s) 28 | return builder.source 29 | }() 30 | // print("GOT:\n-----\n\(source)\n-----") 31 | 32 | XCTAssertTrue(source.contains( 33 | "public struct Person : Identifiable, SQLKeyedTableRecord, Codable")) 34 | } 35 | 36 | func testPersonStructNoLighter() throws { 37 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 38 | let options = Fancifier.Options() 39 | let fancifier = Fancifier(options: options) 40 | fancifier.fancifyDatabaseInfo(dbInfo) 41 | //print("Fancified:", dbInfo) 42 | 43 | let gen = EnlighterASTGenerator( 44 | database: dbInfo, filename: "Contacts.swift" 45 | ) 46 | gen.options.useLighter = false 47 | let s = gen.generateRecordStructure(for: try XCTUnwrap(dbInfo["Person"])) 48 | 49 | let source : String = { 50 | let builder = CodeGenerator() 51 | builder.generateStruct(s) 52 | return builder.source 53 | }() 54 | //print("GOT:\n-----\n\(source)\n-----") 55 | 56 | XCTAssertTrue(source.contains( 57 | "public struct Person : Identifiable, Hashable, Codable")) 58 | XCTAssertFalse(source.contains("SQLKeyedTableRecord")) 59 | } 60 | 61 | 62 | func testTalentStruct() throws { 63 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.talentSchema) 64 | let options = Fancifier.Options() 65 | let fancifier = Fancifier(options: options) 66 | fancifier.fancifyDatabaseInfo(dbInfo) 67 | //print("Fancified:", dbInfo) 68 | 69 | let gen = EnlighterASTGenerator( 70 | database: dbInfo, filename: "Contacts.swift" 71 | ) 72 | let s = gen.generateRecordStructure(for: try XCTUnwrap(dbInfo["Talent"])) 73 | 74 | let source : String = { 75 | let builder = CodeGenerator() 76 | builder.generateStruct(s) 77 | return builder.source 78 | }() 79 | //print("GOT:\n-----\n\(source)\n-----") 80 | 81 | XCTAssertTrue(source.contains( 82 | "public struct Talent : Identifiable, SQLKeyedTableRecord, Codable")) 83 | XCTAssertTrue(source.contains("var id : UUID")) 84 | XCTAssertTrue(source.contains("var name : String")) 85 | 86 | XCTAssertTrue(source.contains("init(id: UUID = UUID(), name: String)")) 87 | XCTAssertTrue(source.contains("self.id = id")) 88 | 89 | XCTAssertFalse(source.contains("has default")) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/EntityGenTests/EntityGenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class EntityGenTests: XCTestCase { 12 | 13 | func testSimpleEntityInfo() throws { 14 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.addressSchema) 15 | //print("GOT DB:", dbInfo) 16 | 17 | XCTAssertEqual(dbInfo.userVersion, 0) 18 | XCTAssertEqual(dbInfo.entities.count, 3) 19 | XCTAssertEqual(dbInfo.entityNames.sorted(), 20 | [ "A Fancy Test Table", "address", "person" ]) 21 | } 22 | 23 | func testNorthWindEntityInfo() throws { 24 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.northWindSchema) 25 | //print("GOT DB:", dbInfo) 26 | 27 | XCTAssertEqual(dbInfo.userVersion, 0) 28 | XCTAssertEqual(dbInfo.entities.count, 14) 29 | XCTAssertEqual( 30 | dbInfo.entityNames.sorted(), 31 | ["Category", "Customer", "CustomerCustomerDemo", "CustomerDemographic", 32 | "Employee", "EmployeeTerritory", "Order", "OrderDetail", "Product", 33 | "ProductDetails_V", "Region", "Shipper", "Supplier", "Territory" ] 34 | ) 35 | } 36 | 37 | func testOGoEntityInfo() throws { 38 | let dbInfo = DatabaseInfo(name: "TestDB", schema: Fixtures.OGoSchema) 39 | 40 | XCTAssertEqual(dbInfo.userVersion, 0) 41 | XCTAssertEqual(dbInfo.entities.count, 63) 42 | XCTAssertEqual( 43 | dbInfo.entityNames.sorted(), 44 | ["address", "appointment", "appointment_resource", 45 | "article", "article_category", "article_unit", 46 | "company", "company_assignment", "company_category", "company_hierarchy", 47 | "company_info", "company_value", "ctags", 48 | "date_company_assignment", "date_info", 49 | "doc", "document", "document_editing", "document_version", 50 | "employment", "enterprise", 51 | "invoice", "invoice_account", "invoice_accounting", "invoice_action", 52 | "invoice_article_assignment", 53 | "job", "job_assignment", "job_history", "job_history_info", 54 | "job_resource_assignment", 55 | "log", "login_token", 56 | "news_article", "news_article_link", 57 | "note", 58 | "obj_info", "obj_property", "object_acl", "object_model", 59 | "palm_address", "palm_category", "palm_date", "palm_memo", "palm_todo", 60 | "person", 61 | "project", "project_acl", "project_companies", 62 | "project_company_assignment", "project_info", "project_persons", 63 | "project_teams", 64 | "resource", "resource_assignment", 65 | "session_log", 66 | "staff", 67 | "table_version", 68 | "team", "team_hierarchy", "team_membership", 69 | "telephone", 70 | "trust"] 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/EntityGenTests/PluralizeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import LighterCodeGenAST 4 | @testable import LighterGeneration 5 | 6 | final class PluralizeTests: XCTestCase { 7 | 8 | func testPluralize() { 9 | XCTAssertEqual("Order" .pluralized, "Orders") 10 | XCTAssertEqual("Category" .pluralized, "Categories") 11 | XCTAssertEqual("Person" .pluralized, "Persons") // wrong 12 | } 13 | 14 | func testSingularize() { 15 | XCTAssertEqual("Orders" .singularized, "Order") 16 | XCTAssertEqual("Categories".singularized, "Category") 17 | XCTAssertEqual("Persons" .singularized, "Person") // wrong 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/FiveThirtyEightTests/FiveThirtyEightTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class FiveThirtyEightTests: XCTestCase { 12 | 13 | let fm = FileManager.default 14 | let url = URL(fileURLWithPath: "/Users/helge/Dropbox/OpenData/FiveThirtyEight.db") 15 | 16 | func testLoadSchema() throws { 17 | try XCTSkipUnless(fm.isReadableFile(atPath: url.path),"helge specific test") 18 | let schema = try SchemaLoader.buildSchemaFromURLs([ url ]) 19 | XCTAssertTrue (schema.views.isEmpty) 20 | XCTAssertFalse(schema.indices.isEmpty) 21 | XCTAssertTrue (schema.triggers.isEmpty) 22 | XCTAssertEqual(schema.tables.count, 388) 23 | //print("SCHEMA:", schema.tables.map(\.name)) 24 | XCTAssertTrue(schema.tables.contains(where: { 25 | $0.name == "world-cup-predictions/wc-20140701-184332" 26 | })) 27 | } 28 | 29 | func testDatabaseMapping() throws { 30 | try XCTSkipUnless(fm.isReadableFile(atPath: url.path),"helge specific test") 31 | let schema = try SchemaLoader.buildSchemaFromURLs([ url ]) 32 | let dbInfo = DatabaseInfo(name: "FiveThirtyEight", schema: schema) 33 | XCTAssertEqual(schema.tables.count, dbInfo.entities.count) 34 | XCTAssertTrue(dbInfo.entities.contains(where: { 35 | $0.name == "world-cup-predictions/wc-20140701-184332" 36 | })) 37 | } 38 | 39 | func testFancifier() throws { 40 | try XCTSkipUnless(fm.isReadableFile(atPath: url.path),"helge specific test") 41 | let schema = try SchemaLoader.buildSchemaFromURLs([ url ]) 42 | let dbInfo = DatabaseInfo(name: "FiveThirtyEight", schema: schema) 43 | 44 | let options = Fancifier.Options() 45 | let fancifier = Fancifier(options: options) 46 | fancifier.fancifyDatabaseInfo(dbInfo) 47 | 48 | XCTAssertEqual(schema.tables.count, dbInfo.entities.count) 49 | 50 | //print("Entities:", dbInfo.entities.map(\.name)) 51 | XCTAssertTrue(dbInfo.entities.contains(where: { 52 | $0.name == "WorldCupPredictions_Wc20140701184332" 53 | })) 54 | } 55 | 56 | func testASTGeneration() throws { 57 | try XCTSkipUnless(fm.isReadableFile(atPath: url.path),"helge specific test") 58 | let schema = try SchemaLoader.buildSchemaFromURLs([ url ]) 59 | 60 | var config = LighterConfiguration.default 61 | config.codeGeneration.rawFunctions = .omit 62 | 63 | let dbInfo = DatabaseInfo(name: "FiveThirtyEight", schema: schema) 64 | let options = Fancifier.Options() 65 | let fancifier = Fancifier(options: options) 66 | fancifier.fancifyDatabaseInfo(dbInfo) 67 | 68 | let gen = EnlighterASTGenerator( 69 | database : dbInfo, 70 | filename : dbInfo.name.appending(".swift"), 71 | options : .init() 72 | ) 73 | let unit = gen.generateCombinedFile(moduleFileName: nil) 74 | #if false 75 | print("UNIT:") 76 | print(" Structures: #\(unit.structures.count)") 77 | print(" Functions: #\(unit.functions.count)") 78 | print(" Extensions: #\(unit.extensions.count)") 79 | #endif 80 | 81 | XCTAssertEqual(schema.tables.count + 1, unit.typeDefinitions.count) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/LighterOperationGenTests/InsertOperationsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class InsertOperationsTests: XCTestCase { 12 | 13 | func testSingleInsert() { 14 | let functions : [ FunctionDefinition ] = { 15 | let generator = InsertFunctionGeneration(columnCount: 1) 16 | generator.generate() 17 | return generator.functions 18 | }() 19 | 20 | let ext = Extension(extendedType: .name("SQLDatabaseChangeOperations"), 21 | functions: functions) 22 | 23 | let source : String = { 24 | let codegen = CodeGenerator() 25 | codegen.generateExtension(ext) 26 | return codegen.source 27 | }() 28 | 29 | //print("GOT:\n-----\n\(source)\n-----") 30 | 31 | XCTAssertTrue(source.contains("func insert(")) 32 | XCTAssertTrue(source.contains( 33 | "into table: KeyPath")) 34 | XCTAssertTrue(source.contains("_ column: KeyPath,")) 35 | XCTAssertTrue(source.contains("values value: C.Value")) 36 | XCTAssertTrue(source.contains( 37 | "where T: SQLTableRecord, C: SQLColumn, T == C.T")) 38 | XCTAssertTrue(source.contains("var builder = SQLBuilder()")) 39 | XCTAssertTrue(source.contains("builder.addColumn(column)")) 40 | XCTAssertTrue(source.contains("try execute(builder")) 41 | XCTAssertTrue(source.contains("readOnly: false")) 42 | } 43 | 44 | func testLargeInsert() { 45 | let functions : [ FunctionDefinition ] = { 46 | let generator = InsertFunctionGeneration(columnCount: 8) 47 | generator.generate() 48 | return generator.functions 49 | }() 50 | 51 | let ext = Extension(extendedType: .name("SQLDatabaseChangeOperations"), 52 | functions: functions) 53 | 54 | let source : String = { 55 | let codegen = CodeGenerator() 56 | codegen.generateExtension(ext) 57 | return codegen.source 58 | }() 59 | 60 | //print("GOT:\n-----\n\(source)\n-----") 61 | 62 | XCTAssertTrue(source.contains("func insert(")) 63 | XCTAssertTrue(source.contains( 64 | "into table: KeyPath")) 65 | XCTAssertTrue(source.contains("_ column: KeyPath,")) 66 | XCTAssertTrue(source.contains("values value: C.Value")) 67 | XCTAssertTrue(source.contains( 68 | "where T: SQLTableRecord, C: SQLColumn, T == C.T")) 69 | XCTAssertTrue(source.contains("var builder = SQLBuilder()")) 70 | XCTAssertTrue(source.contains("builder.addColumn(column)")) 71 | XCTAssertTrue(source.contains("try execute(builder")) 72 | XCTAssertTrue(source.contains("readOnly: false")) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/LighterOperationGenTests/UpdateOperationsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2022 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | @testable import LighterCodeGenAST 9 | @testable import LighterGeneration 10 | 11 | final class UpdateOperationsTests: XCTestCase { 12 | 13 | func testSinglePrimaryKeyUpdate() { 14 | let functions : [ FunctionDefinition ] = { 15 | let generator = UpdateFunctionGeneration( 16 | columnCount: 1, 17 | primaryKey: true 18 | ) 19 | generator.generate() 20 | return generator.functions 21 | }() 22 | 23 | let ext = Extension(extendedType: .name("SQLDatabaseChangeOperations"), 24 | functions: functions) 25 | 26 | let result : String = { 27 | let codegen = CodeGenerator() 28 | codegen.generateExtension(ext) 29 | return codegen.source 30 | }() 31 | 32 | //print("GOT:\n-----\n\(result)\n-----") 33 | 34 | XCTAssertTrue(result.contains("`set` column: KeyPath,")) 35 | XCTAssertTrue(result.contains( 36 | "try execute(builder.sql, builder.bindings, readOnly: false)")) 37 | XCTAssertTrue(result.contains("`is` id: PK.Value")) 38 | } 39 | 40 | func testMultiPrimaryKeyUpdate() { 41 | let functions : [ FunctionDefinition ] = { 42 | let generator = UpdateFunctionGeneration( 43 | columnCount: 6, 44 | primaryKey: true 45 | ) 46 | generator.generate() 47 | return generator.functions 48 | }() 49 | 50 | let ext = Extension(extendedType: .name("SQLDatabaseChangeOperations"), 51 | functions: functions) 52 | 53 | let result : String = { 54 | let codegen = CodeGenerator() 55 | codegen.generateExtension(ext) 56 | return codegen.source 57 | }() 58 | 59 | //print("GOT:\n-----\n\(result)\n-----") 60 | 61 | XCTAssertTrue(result.contains("`set` column5: KeyPath,")) 62 | XCTAssertTrue(result.contains( 63 | "try execute(builder.sql, builder.bindings, readOnly: false)")) 64 | XCTAssertTrue(result.contains("`is` id: PK.Value")) 65 | } 66 | 67 | func testQualifierUpdate() { 68 | let functions : [ FunctionDefinition ] = { 69 | let generator = UpdateFunctionGeneration( 70 | columnCount: 3, 71 | primaryKey: false 72 | ) 73 | generator.generate() 74 | return generator.functions 75 | }() 76 | 77 | let ext = Extension(extendedType: .name("SQLDatabaseChangeOperations"), 78 | functions: functions) 79 | 80 | let result : String = { 81 | let codegen = CodeGenerator() 82 | codegen.generateExtension(ext) 83 | return codegen.source 84 | }() 85 | 86 | //print("GOT:\n-----\n\(result)\n-----") 87 | 88 | XCTAssertTrue(result.contains("`set` column2: KeyPath,")) 89 | XCTAssertTrue(result.contains( 90 | "try execute(builder.sql, builder.bindings, readOnly: false)")) 91 | XCTAssertTrue(result.contains( 92 | "predicate: ( T.Schema ) -> P")) 93 | XCTAssertTrue(result.contains("where: predicate(T.schema)")) 94 | } 95 | } 96 | --------------------------------------------------------------------------------