Compare commits

...

239 Commits

Author SHA1 Message Date
Gwendal Roué
bfa5fbe8ce Documentation tweaks 2019-03-20 16:50:03 +01:00
Gwendal Roué
900134729a Where has count(where:) gone? 2019-03-16 15:31:00 +01:00
Gwendal Roué
d888423676 Fix outdated documentation 2019-03-16 14:32:10 +01:00
Gwendal Roué
eac38be34b Fix for Swift 5 2019-03-16 14:26:06 +01:00
Gwendal Roué
f42a184401
Merge pull request #503 from groue/feature/aggregate-ifnull
IFNULL support for association aggregates
2019-03-15 13:06:56 +01:00
Gwendal Roué
5fbf56c373 KISS 2019-03-15 08:51:02 +01:00
Gwendal Roué
784ebcc8e9 Tighten SQLJoin 2019-03-15 08:41:57 +01:00
Gwendal Roué
df84654554 Delete unused code 2019-03-15 08:23:08 +01:00
Gwendal Roué
53e513f1f8 Merge branch 'GRDB-4.0' into feature/aggregate-ifnull
# Conflicts:
#	CHANGELOG.md
2019-03-15 07:58:42 +01:00
Gwendal Roué
0074adc023
Merge pull request #502 from groue/feature/rename-future
Rename Future to DatabaseFuture
2019-03-15 07:56:56 +01:00
Gwendal Roué
ca8987ad98 Remove ill-advised TODOs 2019-03-14 07:57:30 +01:00
Gwendal Roué
6460f639ee Cleanup: SQLRelation, JoinOperator, JoinCondition are no longer public 2019-03-14 07:54:45 +01:00
Gwendal Roué
41d93f546c Document aggregate operations 2019-03-14 07:46:49 +01:00
Gwendal Roué
80a642260f CHANGELOG 2019-03-14 07:29:47 +01:00
Gwendal Roué
39800b1257 IFNULL support for aggregates 2019-03-14 07:21:09 +01:00
Gwendal Roué
d04fc07337 CHANGELOG 2019-03-14 07:02:39 +01:00
Gwendal Roué
11fd8d6b87 Update documentation 2019-03-14 07:01:39 +01:00
Gwendal Roué
7183457217 Rename Future to DatabaseFuture 2019-03-14 06:43:40 +01:00
Gwendal Roué
cf4e512894 Documentation 2019-03-13 21:24:54 +01:00
Gwendal Roué
cf837855f2 HasManyThroughAssociation and HasOneThroughAssociation doc 2019-03-13 21:19:13 +01:00
Gwendal Roué
916d3931aa Cleanup SQLAssociation 2019-03-13 20:47:55 +01:00
Gwendal Roué
9d9bbc5f5d Documentation 2019-03-13 10:24:00 +01:00
Gwendal Roué
153233ace4 Documentation 2019-03-13 10:16:46 +01:00
Gwendal Roué
4bae8e9ac5 More request(for:association) tests 2019-03-13 08:33:14 +01:00
Gwendal Roué
1e617381a8 AssociationsBasics.md: remove inner link 2019-03-13 07:57:01 +01:00
Gwendal Roué
6d5215c8cb One less contribution needed #499 2019-03-11 09:02:48 +01:00
Gwendal Roué
4ce6271f6d
Merge pull request #499 from groue/feature/EndodableRecord
Extract EncodableRecord from MutablePersistableRecord
2019-03-10 23:01:24 +01:00
Gwendal Roué
10c75d1e5e CHANGELOG 2019-03-10 15:25:39 +01:00
Gwendal Roué
05258b1098 Fix SPM build 2019-03-10 15:21:39 +01:00
Gwendal Roué
1a513e8e6e EncodableRecord documentation 2019-03-10 15:07:05 +01:00
Gwendal Roué
4ae1fb11cc Extract EncodableRecord from MutablePersistableRecord
This fixes issue #426
2019-03-10 14:00:01 +01:00
Gwendal Roué
9500a74744 Merge branch 'development' into GRDB-4.0
# Conflicts:
#	.travis.yml
#	CHANGELOG.md
#	GRDB/QueryInterface/SQLSelectQueryGenerator.swift
#	GRDB/Utils/OrderedDictionary.swift
#	README.md
2019-03-10 12:16:08 +01:00
Gwendal Roué
32f6a45e8f SE-0209: support both swift 4.2 and 5 2019-03-07 20:54:42 +01:00
Gwendal Roué
a355b9eb03
Merge pull request #490 from groue/feature/HasOneThroughAssociation
Indirect Associations
2019-03-06 21:40:47 +01:00
Gwendal Roué
a3c2cb7f1c Merge branch 'GRDB-4.0' into feature/HasOneThroughAssociation 2019-03-06 18:58:05 +01:00
Gwendal Roué
4ba16067d0 Merge branch 'development' into GRDB-4.0 2019-03-06 18:57:06 +01:00
Gwendal Roué
403c4a894f Apply lovely advice from @glessard 2019-03-06 18:18:15 +01:00
Gwendal Roué
7ff21981f0 Restore tests by disabling a sanity check 2019-03-06 09:08:59 +01:00
Gwendal Roué
5d9fa76754 Fix fetchCount(_:) for joined queries 2019-03-06 08:59:06 +01:00
Gwendal Roué
bd633e25ee Aggregate tests for HasManyThroughAssociation 2019-03-06 08:17:47 +01:00
Gwendal Roué
87cd76a468 Association aggregates are defined on AssociationToMany 2019-03-06 08:17:20 +01:00
Gwendal Roué
fca6420053 Documentation 2019-03-06 06:10:18 +01:00
Gwendal Roué
ec51f4dbdf CHANGELOG 2019-03-05 18:54:06 +01:00
Gwendal Roué
916311a851 Remove outdated information 2019-03-05 18:51:58 +01:00
Gwendal Roué
d6b6028e42 Typo fix 2019-03-05 18:49:51 +01:00
Gwendal Roué
c756d27808 Documentation 2019-03-05 18:48:03 +01:00
Gwendal Roué
13985d0553 Merge branch 'GRDB-4.0' into feature/HasOneThroughAssociation 2019-03-05 18:40:12 +01:00
Gwendal Roué
0363ca406d
Merge pull request #493 from groue/feature/SQLite-3.27.2
Bump SQLite to 3.27.2
2019-03-05 18:29:46 +01:00
Gwendal Roué
c20b7e5799 Bump swiftlyfalling/SQLiteLib 2019-03-05 18:29:07 +01:00
Gwendal Roué
7642eb054b Documentation 2019-03-05 08:33:17 +01:00
Gwendal Roué
f3c68420a0 CHANGELOG 2019-03-05 08:05:39 +01:00
Gwendal Roué
223ed4c73c Merge branch 'GRDB-4.0' into feature/SQLite-3.27.2 2019-03-05 08:00:53 +01:00
Gwendal Roué
99d010cded SQLite 3.27.2 2019-03-05 07:58:07 +01:00
Gwendal Roué
5bf1e6f818 Association documentation: contextualize schema recommendations 2019-03-04 20:37:47 +01:00
Gwendal Roué
06f0961b13 Association documentation: avoid painful repetition 2019-03-04 20:13:12 +01:00
Gwendal Roué
addfa45d3a hasMany(_:through:using:) 2019-03-04 08:52:07 +01:00
Gwendal Roué
6489c80846 hasOne(_:through:using:) 2019-03-04 08:49:03 +01:00
Gwendal Roué
9e8e00ff99 AssociationHasManySQLTests 2019-03-04 08:06:50 +01:00
Gwendal Roué
9ee25fdec1 Rename ToOneAssociation to AssociationToOne 2019-03-04 08:01:45 +01:00
Gwendal Roué
4d5e5e1de3 HasManyThroughAssociation 2019-03-03 18:35:05 +01:00
Gwendal Roué
1188969144 Cleanup 2019-03-03 17:34:23 +01:00
Gwendal Roué
66c72e7bd2 HasOneThroughAssociation request tests 2019-03-03 15:29:27 +01:00
Gwendal Roué
eed30c0954 AssociationHasOneThroughDecodableRecordTests 2019-03-03 09:21:49 +01:00
Gwendal Roué
8dd9f5fd40 AssociationHasOneThroughFetchableRecordTests cleanup 2019-03-03 09:21:34 +01:00
Gwendal Roué
7cab5dbf2b Cleanup 2019-03-02 20:42:52 +01:00
Gwendal Roué
6fd14d0a70 AssociationHasOneThroughFetchableRecordTests 2019-03-02 20:41:52 +01:00
Gwendal Roué
51c0b7a2f8 Renamed AssociationHasOneThroughRowscopeTests 2019-03-02 20:26:31 +01:00
Gwendal Roué
679f989e96 More AssociationHasOneThroughSQLDerivationTests 2019-03-02 17:35:00 +01:00
Gwendal Roué
1a24f61723 HasOneThroughAssociation adopts TableRequest 2019-03-02 17:24:17 +01:00
Gwendal Roué
1ff5552d5e AssociationHasOneThroughSQLDerivationTests 2019-03-02 17:14:58 +01:00
Gwendal Roué
4815807f09 More AssociationHasOneThroughSQLTests 2019-03-02 17:01:27 +01:00
Gwendal Roué
8fc23e1771 Complete AssociationHasOneThroughSQLTests 2019-03-02 15:48:17 +01:00
Gwendal Roué
5cf2d4920d TODO 2019-03-02 15:37:44 +01:00
Gwendal Roué
3d6d1c89b2 HasOneThroughAssociation 2019-03-02 14:46:31 +01:00
Gwendal Roué
08451dfced Rename AssociationImpl to SQLAssociation 2019-03-02 14:46:12 +01:00
Gwendal Roué
b506838519 ToOneAssociation 2019-03-01 19:13:46 +01:00
Gwendal Roué
1511981dc7 AssociationImpl is no longer a protocol 2019-03-01 19:13:28 +01:00
Gwendal Roué
661cca9f80
Merge pull request #488 from groue/feature/ValueObservationCleanup
ValueObservation Cleanup
2019-03-01 16:55:24 +01:00
Gwendal Roué
4ee2523d59 CHANGELOG 2019-03-01 13:51:22 +01:00
Gwendal Roué
997be18718 Update ValueObservation documentation 2019-03-01 13:51:15 +01:00
Gwendal Roué
e966544aeb Remove ValueObservation.extent 2019-03-01 13:17:48 +01:00
Gwendal Roué
4935c6f258 Rename ValueScheduling.onQueue(_:startImmediately:) to .async(onQueue:startImmediately:) 2019-03-01 08:24:00 +01:00
Gwendal Roué
03fafa437c Cleanup 2019-03-01 08:05:46 +01:00
Gwendal Roué
0fe7db2694 Assume NSUUID.fromDatabaseValue no longer needs special care in Release configuration, since we target Xcode 10+ now 2019-02-28 13:28:06 +01:00
Gwendal Roué
22edb91796
Merge pull request #487 from groue/feature/SR-6067
Remove SR-6067 workaround
2019-02-28 12:15:41 +01:00
Gwendal Roué
5ffc35a955 Remove SR-6067 workaround 2019-02-28 08:35:16 +01:00
Gwendal Roué
1084fe0425 Cleanup 2019-02-26 20:45:58 +01:00
Gwendal Roué
8d867320b0 Remove DatabaseValue.losslessConvert methods
They were used by RxGRDB until 0.13.0.
2019-02-26 20:40:13 +01:00
Gwendal Roué
34dd011e90 TODO: replace ValueObservation.extent 2019-02-26 20:23:14 +01:00
Gwendal Roué
8fce4faeea Fix typo 2019-02-26 09:00:29 +01:00
Gwendal Roué
8faff704d5
Merge pull request #486 from groue/feature/RecordNotFound
Refactor PersistenceError.recordNotFound
2019-02-26 08:09:04 +01:00
Gwendal Roué
2d71679476 Cleanup 2019-02-26 08:07:13 +01:00
Gwendal Roué
715b7a7867 CHANGELOG 2019-02-25 20:48:35 +01:00
Gwendal Roué
8ef28b0683 Fix PersistenceError.description 2019-02-25 20:43:40 +01:00
Gwendal Roué
31bdb2eeaa PersistenceError.recordNotFound documentation 2019-02-25 20:21:20 +01:00
Gwendal Roué
63192e2b75 Refactor PersistenceError.recordNotFound 2019-02-25 20:17:49 +01:00
Gwendal Roué
fcbbe17909 Documentation 2019-02-25 18:53:32 +01:00
Gwendal Roué
a1e809cbc7 Cleanup TODO 2019-02-25 17:23:02 +01:00
Gwendal Roué
93b429cca4 CHANGELOG 2019-02-24 18:02:57 +01:00
Gwendal Roué
3aaa5e74ec Update README 2019-02-24 17:48:10 +01:00
Gwendal Roué
27156fd215 Support for remove_diacritics=2 in FTS5 2019-02-24 17:38:04 +01:00
Gwendal Roué
bcac197910 [whitespace] FTS5 files indentation 2019-02-24 17:12:10 +01:00
Gwendal Roué
336081a78a Support for remove_diacritics=2 in FTS3/4 2019-02-24 17:06:44 +01:00
Gwendal Roué
6306bd31e3 Update SQLiiteCustom/src to branch 3.27.1 of groue/SQLiteLib 2019-02-24 16:00:17 +01:00
Gwendal Roué
690e980018 Fix double inclusion of GRDB-4.0.swift in GRDBWatchOS target 2019-02-24 15:40:15 +01:00
Gwendal Roué
f9fbd42962 CHANGELOG 2019-02-24 15:22:40 +01:00
Gwendal Roué
919f6095c6
Merge pull request #484 from groue/feature/SE-0193
SE-0193 Cross-module inlining and specialization
2019-02-24 15:18:49 +01:00
Gwendal Roué
83f62eed79 CHANGELOG 2019-02-24 15:18:04 +01:00
Gwendal Roué
96b971b0da PersistenceContainer specialization 2019-02-24 14:42:00 +01:00
Gwendal Roué
05fe018fed GRDBProfiling enhancements 2019-02-24 06:28:35 +01:00
Gwendal Roué
921c5d636a Remove SQLite.swift dependency from GRDBOSXPerformanceTests target 2019-02-24 06:26:51 +01:00
Gwendal Roué
61ae460525 Persistence methods specialization, PersistenceContainer optimizations 2019-02-24 06:26:08 +01:00
Gwendal Roué
caaf2365e0 Fix compiler warning: make SQLiteValue @usableFromInline 2019-02-23 20:31:56 +01:00
Gwendal Roué
bed9164bcd Allow specialization of fetching methods 2019-02-23 18:34:42 +01:00
Gwendal Roué
f3621da329 @inlinable remaining Row subscripts 2019-02-23 17:37:04 +01:00
Gwendal Roué
2c75582c22 @inlinable RecordCursor 2019-02-23 17:23:30 +01:00
Gwendal Roué
aa4f7c3d78 @inlinable StatementCursor 2019-02-23 17:19:56 +01:00
Gwendal Roué
9906cf799c @inlinable DatabaseValueCursor and NullableDatabaseValueCursor 2019-02-23 17:04:32 +01:00
Gwendal Roué
304937161c Performance comparison: Realm 3.13.1 2019-02-23 16:17:23 +01:00
Gwendal Roué
789497da75 FMDatabaseQueue(path:) is a failable initializer 2019-02-23 16:08:24 +01:00
Gwendal Roué
14371cd7c6 Performance comparison: update Realm location 2019-02-23 15:57:56 +01:00
Gwendal Roué
b9dbce47a2 Performance comparison: FMDB 2.7.5 2019-02-23 15:46:09 +01:00
Gwendal Roué
eb1672dec3 Revert wrong Xcode refactoring 2019-02-23 15:41:38 +01:00
Gwendal Roué
dd0926ebfb ValueConversionContext does not need to be @usableFromInline 2019-02-23 15:35:23 +01:00
Gwendal Roué
e80f3639af Revert wrong Xcode refactoring 2019-02-23 15:34:25 +01:00
Gwendal Roué
944b3ac07d @inlinable RowCursor, and Row subscript returning StatementColumnConvertible 2019-02-23 15:27:12 +01:00
Gwendal Roué
746c22e950 @inlinable FastDatabaseValueCursor and FastNullableDatabaseValueCursor 2019-02-23 14:28:44 +01:00
Gwendal Roué
a0c85f093c Remove all @inline(__always) 2019-02-23 13:37:00 +01:00
Gwendal Roué
728c3b28ed Typo fix 2019-02-22 14:28:36 +01:00
Gwendal Roué
03b8df2fe1
Merge pull request #478 from groue/feature/SQLInterpolation2
Swift 5: SQL interpolation
2019-02-22 13:43:11 +01:00
Gwendal Roué
dae55e4105 Merge branch 'GRDB-4.0' into feature/SQLInterpolation2 2019-02-22 13:37:44 +01:00
Gwendal Roué
f8a1ecb644 Use query variable name when an SQLLiteral contains a full SQL query 2019-02-22 13:34:30 +01:00
Gwendal Roué
22c2cfb4ea Test SQLRequest interpolation 2019-02-22 13:28:58 +01:00
Gwendal Roué
b15eaa826b TODO 2019-02-22 13:07:27 +01:00
Gwendal Roué
32b8d87a64 Use sqlite3_prepare_v3 on iOS 12.0, OSX 10.14, watchOS 5.0 2019-02-22 13:06:24 +01:00
Gwendal Roué
62ba97f517 Inline documentation 2019-02-22 12:59:44 +01:00
Gwendal Roué
e81a514f29 SQLInterpolation.md 2019-02-22 09:15:05 +01:00
Gwendal Roué
e36ca9f645 Foster SQL interpolation 2019-02-21 23:23:35 +01:00
Gwendal Roué
d9dc21a073 Merge branch 'GRDB-4.0' into feature/SQLInterpolation2 2019-02-21 23:11:58 +01:00
Gwendal Roué
59cc1fef67 SQLInterpolation.md 2019-02-21 23:10:06 +01:00
Gwendal Roué
8df8129743 SQLInterpolation.appendInterpolation(SQLRequest) 2019-02-21 14:20:50 +01:00
Gwendal Roué
b6481e8a68 SQLInterpolation.md 2019-02-21 12:20:16 +01:00
Gwendal Roué
65b65f4d70 SQL Interpolation documentation 2019-02-21 08:51:01 +01:00
Gwendal Roué
870ef6bac8 CHANGELOG 2019-02-21 08:48:22 +01:00
Gwendal Roué
f7f0810c17 Documentation 2019-02-20 14:08:31 +01:00
Gwendal Roué
17c3ac1911 Link to #482 2019-02-20 08:54:35 +01:00
Gwendal Roué
d74340a317 GRDB 4 development process 2019-02-20 08:17:23 +01:00
Gwendal Roué
58dc1c67d6 Fix some sample code 2019-02-17 13:28:41 +01:00
Gwendal Roué
7e9e50e52d Up to date playgrounds 2019-02-17 13:28:41 +01:00
Gwendal Roué
df05e940e8 Minor 2019-02-17 13:28:41 +01:00
Gwendal Roué
8d6a2fb4f1 Documentation, support for interpolation of empty sequences 2019-02-17 13:28:41 +01:00
Gwendal Roué
afa8360397 Restore SQLExpressionLiteral.sql and arguments 2019-02-17 13:28:41 +01:00
Gwendal Roué
50f8bcec34 Fixit for SQLExpression.literal 2019-02-17 13:28:41 +01:00
Gwendal Roué
d13968b753 Documentation 2019-02-17 13:28:41 +01:00
Gwendal Roué
086e3e918e joined(separator:) for SQLLiteral sequences 2019-02-17 13:28:41 +01:00
Gwendal Roué
e986f0d6e5 Test for interpolation of empty sequence 2019-02-17 13:28:41 +01:00
Gwendal Roué
36ce78bd37 Explicit sql argument label when building prepared statements 2019-02-17 13:28:41 +01:00
Gwendal Roué
e2a11eeb05 Update some inline documentation 2019-02-17 13:28:41 +01:00
Gwendal Roué
2d40b86a50 Rename SQLLiteral.append(literal:) to append(_:) 2019-02-17 13:28:41 +01:00
Gwendal Roué
4f350631ff Remove fetching methods from SQLLiteral. 2019-02-17 13:28:41 +01:00
Gwendal Roué
db67d3840b Principle of least astonishment: rrename argument label rawSQL to sql 2019-02-17 13:28:41 +01:00
Gwendal Roué
2cd20b79f7 Fix typo 2019-02-17 13:28:40 +01:00
Gwendal Roué
b296e9824a SQLGenerationContext.append(arguments:) cleanup 2019-02-17 13:28:40 +01:00
Gwendal Roué
b0d9c519e1 Fix SPM test 2019-02-17 13:28:40 +01:00
Gwendal Roué
6937193d28 Fix SPM compilation 2019-02-17 13:28:40 +01:00
Gwendal Roué
500a5eea28 SQLRequest(rawSQL:arguments:adapter:) 2019-02-17 13:28:40 +01:00
Gwendal Roué
9758e1783e [whitespace] 2019-02-17 13:28:40 +01:00
Gwendal Roué
a57aa2e346 rawSQL migration complete 2019-02-17 13:28:40 +01:00
Gwendal Roué
4bd5ace2ee Use rawSQL as parameter name for raw SQL 2019-02-17 13:28:40 +01:00
Gwendal Roué
85f618e16e SQLLiteral.mapSQL 2019-02-17 13:28:40 +01:00
Gwendal Roué
e7995b9cd4 SQLExpressionLiteral(rawSQL:) 2019-02-17 13:28:40 +01:00
Gwendal Roué
6e80c748d2 SQLRequest(rawSQL:arguments:adapter:) 2019-02-17 13:28:40 +01:00
Gwendal Roué
c546c578ce fetchCursor/All/One(_:rawSQL:arguments:adapter:) 2019-02-17 13:28:40 +01:00
Gwendal Roué
59162d93de Rename Database.execute(_:String) to Database.execute(rawSQL:String) 2019-02-17 13:27:54 +01:00
Gwendal Roué
354d22d70d Update documentation for literal interpolation 2019-02-17 13:24:38 +01:00
Gwendal Roué
f15b2e12d6 Remove support for check constraint defined with a literal
That's because we can't support it until SQLLiteral is able to quote values
2019-02-17 13:24:38 +01:00
Gwendal Roué
4ecef0ea11 Don't be shy on interpolation 2019-02-17 13:24:38 +01:00
Gwendal Roué
637de8b6fa SQLiteral are passed via a literal parameter 2019-02-17 13:24:38 +01:00
Gwendal Roué
aa6f55d72f Statement arguments are generally non optional 2019-02-17 13:24:38 +01:00
Gwendal Roué
f018c7f823 Rename SQLString to SQLLiteral 2019-02-17 13:24:38 +01:00
Gwendal Roué
98e2d009c6 SQLString tests 2019-02-17 13:24:38 +01:00
Gwendal Roué
09e85ba341 SQLString everywhere 2019-02-17 13:24:38 +01:00
Gwendal Roué
f27a1e9504 request.select(SQLString) 2019-02-17 13:24:38 +01:00
Gwendal Roué
86409687a4 TODO: Documentation 2019-02-17 13:24:38 +01:00
Gwendal Roué
ff439c2dbc Documentation 2019-02-17 13:24:37 +01:00
Gwendal Roué
9b6c9b421a Update CustomizedDecodingOfDatabaseRows for SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
b5bcdd9df4 Fetch FetchableRecord from SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
b36cf84b23 Fetch StatementColumnConvertible from SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
fb89b540cd Fetch DatabaseValueConvertible from SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
86b8c35c04 Fetch rows from SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
e7497e707e SQLRequest.init(SQLString) 2019-02-17 13:24:37 +01:00
Gwendal Roué
b7089bb025 Database.execute(SQLString) 2019-02-17 13:24:37 +01:00
Gwendal Roué
612bd70710 Test SQLString in Swift 4.2 mode 2019-02-17 13:24:37 +01:00
Gwendal Roué
8affc8abe4 Remove dead code 2019-02-17 13:24:37 +01:00
Gwendal Roué
d9cf760a25 SQLString 2019-02-17 13:24:37 +01:00
Gwendal Roué
5b6d882d67 SQLRequest adopts ExpressibleByStringInterpolation 2019-02-17 13:24:37 +01:00
Gwendal Roué
87ee00c2c5 SQLInterpolation 2019-02-17 13:24:37 +01:00
Gwendal Roué
e100287bd8 Fixits for ValueObservation 2019-02-17 13:09:19 +01:00
Gwendal Roué
323b8de3dc Fixits for ColumnCursor and NullableColumnCursor 2019-02-17 13:02:48 +01:00
Gwendal Roué
06ec4580be Fixit for DatabaseWriter.readFromCurrentState 2019-02-17 12:59:32 +01:00
Gwendal Roué
efd69a628a Add fixit for Cursor.flatMap 2019-02-17 12:50:29 +01:00
Gwendal Roué
cc80daec99 Fixit/GRDB-4.0.swift 2019-02-17 12:48:13 +01:00
Gwendal Roué
fd398aa434 Fix cocapods lint tests 2019-02-17 11:09:56 +01:00
Gwendal Roué
4b8ea08f4f Simplify Statement preparation now that SR-2347 is solved 2019-02-10 14:20:12 +01:00
Gwendal Roué
e57cf49dc4 Remove useless check 2019-02-09 20:27:15 +01:00
Gwendal Roué
111dcbcad4 Fix warnings 2019-02-09 20:10:11 +01:00
Gwendal Roué
4174896225 Fix DataMemoryTests 2019-02-09 16:05:03 +01:00
Gwendal Roué
5738edc1bb CHANGELOG 2019-02-09 14:25:33 +01:00
Gwendal Roué
192bd71c12 SQLRequest.arguments is no longer optional 2019-02-09 14:14:07 +01:00
Gwendal Roué
5ee3b9c878 SE-0235 Add Result to the Standard Library 2019-02-09 12:54:30 +01:00
Gwendal Roué
d3020dbd8f Workaround SR-9893
https://bugs.swift.org/browse/SR-9893
2019-02-09 11:10:27 +01:00
Gwendal Roué
493575a47b SE-0220 count(where:) 2019-02-09 10:21:34 +01:00
Gwendal Roué
fa4febfa93 GRDB 4.0 TODO 2019-02-09 10:04:24 +01:00
Gwendal Roué
8cd49082ec SE-0205 withUnsafePointer(to:_:) and withUnsafeBytes(of:_:) for immutable values 2019-02-09 09:56:12 +01:00
Gwendal Roué
0dbfe7d9e3 GRDB 4.0 TODO 2019-02-09 09:50:32 +01:00
Gwendal Roué
bbc64e38c7 SE-0207 Add an allSatisfy algorithm to Sequence 2019-02-09 09:50:04 +01:00
Gwendal Roué
d7188f4b93 TODO: Rename Future 2019-02-08 10:19:01 +01:00
Gwendal Roué
bb045235bf Remove tests for deprecated methods 2019-02-08 10:16:17 +01:00
Gwendal Roué
ca4879c496 Remove deprecated ValueObservation methods 2019-02-08 10:16:17 +01:00
Gwendal Roué
a466a8b253 Remove deprecated Cursor types 2019-02-08 10:16:17 +01:00
Gwendal Roué
5cd66c1be8 Remove deprecated Cursor.flatMap 2019-02-08 10:16:17 +01:00
Gwendal Roué
4489106bd6 Remove deprecated DatabaseWriter.readFromCurrentState 2019-02-08 10:16:17 +01:00
Gwendal Roué
485d42373b GRDB 4 is the latest version for Swift 4.2 2019-02-08 09:00:34 +01:00
Gwendal Roué
249484a21f SE-0204 Add last(where:) and lastIndex(where:) Methods 2019-02-08 08:47:45 +01:00
Gwendal Roué
fb8946254d Swift 5: update calls to Data.withUnsafeBytes 2019-02-08 08:22:23 +01:00
Gwendal Roué
b8a2592737 TODO future support for SQLite 3.27.0 2019-02-08 07:55:07 +01:00
Gwendal Roué
3029949731 Add () to forward @autoclosure parameters 2019-02-08 05:30:57 +01:00
Gwendal Roué
91061ee669 Remove redundant public modifiers 2019-02-08 05:27:50 +01:00
Gwendal Roué
90a9aa3523 Fix SPM compilation 2019-02-07 12:57:09 +01:00
Gwendal Roué
ad04ccc094 Drop all fixits for GRDB < 3.0 2019-02-07 12:52:52 +01:00
Gwendal Roué
a230fe400d Availability checks: assume iOS >= 9.0 2019-02-07 08:52:38 +01:00
Gwendal Roué
62ef344a6c Simplify FTS5, assuming swift(>=4.2) 2019-02-07 08:39:06 +01:00
Gwendal Roué
3eb36768c6 Simplify iOS memory management, assuming swift(>=4.2) 2019-02-07 08:25:31 +01:00
Gwendal Roué
b6f79f176c SE-0206 : Hashable Enhancements 2019-02-07 08:21:48 +01:00
Gwendal Roué
e2d151e748 Travis: test Xcode 10.1 2019-02-06 21:30:49 +01:00
Gwendal Roué
54ddd3e025 Set IPHONEOS_DEPLOYMENT_TARGET to 9.0 2019-02-06 20:05:53 +01:00
Gwendal Roué
2d7857f45b Package.swift uses swift-tools-version:4.2 2019-02-06 20:04:39 +01:00
Gwendal Roué
81892491f2 Set default SWIFT_VERSION to 4.2 2019-02-06 19:53:00 +01:00
Gwendal Roué
0493a9646f GRDB 4.0 kickoff 2019-02-06 19:45:19 +01:00
293 changed files with 14567 additions and 8589 deletions

2
.gitmodules vendored
View File

@ -12,4 +12,4 @@
url = https://github.com/groue/sqlcipher.git
[submodule "SQLiteCustom/src"]
path = SQLiteCustom/src
url = https://github.com/swiftlyfalling/SQLiteLib
url = https://github.com/swiftlyfalling/SQLiteLib.git

View File

@ -1 +1 @@
4.1
4.2

View File

@ -13,186 +13,197 @@ jobs:
include:
###########################################
## Test GRDB
## Test GRDB Xcode 10.1
# Test GRDBOSX (Xcode 10)
- stage: Test GRDB
- stage: Test GRDB Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBOSX (Xcode 10, Swift 4.2, macOS)
- TID=GRDBOSX (Swift 4.2, macOS)
script: make test_framework_GRDBOSX_maxSwift
# Test GRDBOSX (Xcode 10)
- stage: Test GRDB
- stage: Test GRDB Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBOSX (Xcode 10, Swift 4.0, macOS)
script: make test_framework_GRDBOSX_minSwift
# Test GRDBWatchOS (Xcode 10)
- stage: Test GRDB
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBWatchOS (Xcode 10, watchOS)
- TID=GRDBWatchOS (watchOS)
script: make test_framework_GRDBWatchOS
# Test GRDBiOS (Xcode 10, iOS <MAXIMUM VERSION>)
- stage: Test GRDB
- stage: Test GRDB Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBiOS (Xcode 10, Swift 4.2, iOS <MAX>)
- TID=GRDBiOS (Swift 4.2, iOS <MAX>)
script: make test_framework_GRDBiOS_maxTarget_maxSwift
# Test GRDBiOS (Xcode 9.4, iOS <MAXIMUM VERSION>)
- stage: Test GRDB
- stage: Test GRDB Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.4
osx_image: xcode10.1
env:
- TID=GRDBiOS (Xcode 9.4, iOS <MAX>)
script: make test_framework_GRDBiOS_maxTarget_maxSwift
# Test GRDBiOS (Xcode 9.3, iOS <MINIMUM VERSION>))
- stage: Test GRDB
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.3
env:
- TID=GRDBiOS (Xcode 9.3, iOS <MIN>)
- TID=GRDBiOS (Swift 4.2, iOS <MIN>)
script: make test_framework_GRDBiOS_minTarget
# Test GRDB [SPM] (Xcode 10, macOS)
- stage: Test GRDB
- stage: Test GRDB Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDB [SPM] (Xcode 10, macOS)
- TID=GRDB [SPM] (macOS)
script: make test_SPM
###########################################
## Test GRDB (Custom SQLite)
## Test GRDB Xcode 10
# Test GRDBCustomSQLiteOSX (Xcode 10)
- stage: Test GRDB + Custom SQLite
- stage: Test GRDB Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBCustomSQLiteOSX (Xcode 10, macOS)
- TID=GRDBOSX (Swift 4.2, macOS)
script: make test_framework_GRDBOSX_maxSwift
- stage: Test GRDB Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBWatchOS (watchOS)
script: make test_framework_GRDBWatchOS
- stage: Test GRDB Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBiOS (Swift 4.2, iOS <MAX>)
script: make test_framework_GRDBiOS_maxTarget_maxSwift
- stage: Test GRDB Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBiOS (Swift 4.2, iOS <MIN>)
script: make test_framework_GRDBiOS_minTarget
- stage: Test GRDB Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDB [SPM] (macOS)
script: make test_SPM
###########################################
## Test GRDBCustom Xcode 10.1
- stage: Test GRDBCustom Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10.1
env:
- TID=GRDBOSX (Swift 4.2, macOS)
script: make test_framework_GRDBCustomSQLiteOSX
# Test GRDBCustomSQLiteiOS (Xcode 10, iOS <MAXIMUM VERSION>)
- stage: Test GRDB + Custom SQLite
- stage: Test GRDBCustom Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBCustomSQLiteiOS (Xcode 10, Swift 4.2, iOS <MAX>)
- TID=GRDBiOS (Swift 4.2, iOS <MAX>)
script: make test_framework_GRDBCustomSQLiteiOS_maxTarget_maxSwift
# Test GRDBCustomSQLiteiOS (Xcode 10, iOS <MAXIMUM VERSION>)
- stage: Test GRDB + Custom SQLite
- stage: Test GRDBCustom Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBCustomSQLiteiOS (Xcode 10, Swift 4.0, iOS <MAX>)
script: make test_framework_GRDBCustomSQLiteiOS_maxTarget_minSwift
# Test GRDBCustomSQLiteiOS (Xcode 9.4, iOS <MAXIMUM VERSION>)
- stage: Test GRDB + Custom SQLite
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.4
env:
- TID=GRDBCustomSQLiteiOS (Xcode 9.4, iOS <MAX>)
script: make test_framework_GRDBCustomSQLiteiOS_maxTarget
# Test GRDBCustomSQLiteiOS (Xcode 9.3, iOS <MINIMUM VERSION>))
- stage: Test GRDB + Custom SQLite
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.3
env:
- TID=GRDBCustomSQLiteiOS (Xcode 9.3, iOS <MIN>)
- TID=GRDBiOS (Swift 4.2, iOS <MIN>)
script: make test_framework_GRDBCustomSQLiteiOS_minTarget
###########################################
## Test GRDB (SQLCipher)
## Test GRDBCustom Xcode 10
# Test GRDBCipherOSX (Xcode 10)
- stage: Test GRDB + SQLCipher
- stage: Test GRDBCustom Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBCipherOSX (Xcode 10, macOS)
- TID=GRDBOSX (Swift 4.2, macOS)
script: make test_framework_GRDBCustomSQLiteOSX
- stage: Test GRDBCustom Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBiOS (Swift 4.2, iOS <MAX>)
script: make test_framework_GRDBCustomSQLiteiOS_maxTarget_maxSwift
- stage: Test GRDBCustom Xcode 10
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
env:
- TID=GRDBiOS (Swift 4.2, iOS <MIN>)
script: make test_framework_GRDBCustomSQLiteiOS_minTarget
###########################################
## Test GRDBCipher Xcode 10.1
- stage: Test GRDBCipher Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10.1
env:
- TID=GRDBOSX (Swift 4.2, macOS)
script: make test_framework_GRDBCipherOSX
# Test GRDBCipheriOS (Xcode 10, iOS <MAXIMUM VERSION>)
- stage: Test GRDB + SQLCipher
- stage: Test GRDBCipher Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=GRDBCipheriOS (Xcode 10, iOS <MAX>)
- TID=GRDBiOS (Swift 4.2, iOS <MAX>)
script: make test_framework_GRDBCipheriOS_maxTarget
# Test GRDBCipheriOS (Xcode 9.4, iOS <MAXIMUM VERSION>)
- stage: Test GRDB + SQLCipher
- stage: Test GRDBCipher Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.4
osx_image: xcode10.1
env:
- TID=GRDBCipheriOS (Xcode 9.4, iOS <MAX>)
script: make test_framework_GRDBCipheriOS_maxTarget
# Test GRDBCipheriOS (Xcode 9.3, iOS <MINIMUM VERSION>))
- stage: Test GRDB + SQLCipher
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode9.3
env:
- TID=GRDBCipheriOS (Xcode 9.3, iOS <MIN>)
- TID=GRDBiOS (Swift 4.2, iOS <MIN>)
script: make test_framework_GRDBCipheriOS_minTarget
###########################################
## Test Installation Methods
## Test Installation Xcode 10.1
# Manual Install (Xcode 10)
- stage: Test Installation
# Manual Install
- stage: Test Installation Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=Manual Install (Xcode 10)
- TID=Manual Install
script: make test_install_manual
# Manual Install (GRDBCipher, Xcode 10)
- stage: Test Installation
- stage: Test Installation Xcode 10.1
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode10
osx_image: xcode10.1
env:
- TID=Manual Install (GRDBCipher, Xcode 10)
script: make test_install_GRDBCipher
# CocoaPods Lint (Xcode 10)
- stage: Test Installation
osx_image: xcode10
# CocoaPods Lint
- stage: Test Installation Xcode 10.1
osx_image: xcode10.1
install:
- gem install cocoapods --pre # >= 1.6.0.beta.1 for cocoapods lint
- gem install cocoapods # >= 1.6.0 for cocoapods lint
env:
- TID=CocoaPods Lint GRDB.swift (Xcode 10)
- TID=CocoaPods Lint GRDB.swift
script: make test_CocoaPodsLint_GRDB
# CocoaPods Lint (Xcode 10)
- stage: Test Installation
osx_image: xcode10
# CocoaPods Lint
- stage: Test Installation Xcode 10.1
osx_image: xcode10.1
install:
- gem install cocoapods --pre # >= 1.6.0.beta.1 for cocoapods lint
- gem install cocoapods # >= 1.6.0 for cocoapods lint
env:
- TID=CocoaPods Lint GRDBCipher (Xcode 10)
script: make test_CocoaPodsLint_GRDBCipher
# CocoaPods Install (Xcode 10)
- stage: Test Installation
osx_image: xcode10
# CocoaPods Install
- stage: Test Installation Xcode 10.1
osx_image: xcode10.1
install:
- gem install cocoapods --pre # >= 1.6.0.beta.1 for cocoapods lint
- gem install cocoapods # >= 1.6.0 for cocoapods lint
env:
- TID=CocoaPods GRDB (Xcode 10)
- TID=CocoaPods GRDB
script: make test_install_GRDB_CocoaPods
# Disabled until we understand the reason for this failure:
@ -207,16 +218,16 @@ jobs:
# - TID=CocoaPods GRDBCipher (Xcode 10)
# script: make test_install_GRDBCipher_CocoaPods
# SPM Install (Xcode 10)
- stage: Test Installation
osx_image: xcode10
# SPM Install
- stage: Test Installation Xcode 10.1
osx_image: xcode10.1
env:
- TID=SPM (Xcode 10)
- TID=SPM
script: make test_install_SPM
## Carthage Build
- stage: Test Installation
osx_image: xcode10
- stage: Test Installation Xcode 10.1
osx_image: xcode10.1
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage

View File

@ -3,7 +3,11 @@ Release Notes
All notable changes to this project will be documented in this file.
GRDB adheres to [Semantic Versioning](https://semver.org/).
GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection: APIs flagged [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features). Those are unstable, and may break between any two minor releases of the library.
#### 4.x Releases
- `4.0.0` Development - [GRDB-4.0 Branch](#grdb-40-branch)
#### 3.x Releases
@ -43,6 +47,33 @@ GRDB adheres to [Semantic Versioning](https://semver.org/).
- [0.110.0](#01100), ...
## GRDB-4.0 Branch
### New
- [#478](https://github.com/groue/GRDB.swift/pull/478): Swift 5: SQL interpolation
- [#484](https://github.com/groue/GRDB.swift/pull/484): SE-0193 Cross-module inlining and specialization
- [#486](https://github.com/groue/GRDB.swift/pull/486): Refactor PersistenceError.recordNotFound
- [#488](https://github.com/groue/GRDB.swift/pull/488): ValueObservation Cleanup
- [#490](https://github.com/groue/GRDB.swift/pull/490): Indirect Associations
- [#493](https://github.com/groue/GRDB.swift/pull/493): Bump SQLite to [3.27.2](https://www.sqlite.org/releaselog/3_27_2.html)
- [#499](https://github.com/groue/GRDB.swift/pull/499): Extract EncodableRecord from MutablePersistableRecord
- [#502](https://github.com/groue/GRDB.swift/pull/502): Rename Future to DatabaseFuture
- [#503](https://github.com/groue/GRDB.swift/pull/503): IFNULL support for association aggregates
### Breaking Changes
- Swift 4.0 and Swift 4.1 are no longer supported
- iOS 8 is no longer supported. Minimum deployment target is now iOS 9.0
- Deprecated APIs are no longer available.
### Documentation Diff
- [SQL Interpolation](Documentation/SQLInterpolation.md): this new document describes the new SQL interpolation feature.
- [Required Protocols](Documentation/AssociationsBasics.md#required-protocols) describes which protocols your record types have to conform to in order to use Associations features.
- [Aggregate Operations](Documentation/AssociationsBasics.md#aggregate-operations) describes all the ways to transform association aggregates with logical, comparison and arithmetic operators.
## 3.7.0
Released March 9, 2019 &bull; [diff](https://github.com/groue/GRDB.swift/compare/v3.6.2...v3.7.20)

View File

@ -86,7 +86,6 @@ The ideas, in alphabetical order:
- [Linux]
- [More SQL Generation]
- [Reactive Database Observation]
- [Records: Splitting Database Encoding from Ability to Write in the Database]
- [SQL Console in the Debugger]
- [SQLCipher in a Shared App Container]
- [Static Library]
@ -303,19 +302,6 @@ We already have the [RxGRDB] companion library, which offers [RxSwift](https://g
We need more choices of reactive engines.
### Records: Splitting Database Encoding from Ability to Write in the Database
:baby: Starter Task :pencil: Documentation
Record types that know how to encode themselves in the database (converting themselves into columns and database values) currently are granted with [persistence methods] which can write in the database. Those record types all adopt the [PersistableRecord] protocol.
But encoding a record grants other features, such as [Record Comparison], or [Requesting Associated Records].
This is a problem: one should not have to grant a type with persistence methods, when one just needs read-only features.
The fix is to split database encoding from persistence methods. See [#426](https://github.com/groue/GRDB.swift/issues/426) for more information.
### SQL Console in the Debugger
:question: Unknown Difficulty :hammer: Tooling

View File

@ -2,10 +2,13 @@ GRDB Associations
=================
- [Associations Benefits]
- [Required Protocols]
- [The Types of Associations]
- [BelongsTo]
- [HasOne]
- [HasMany]
- [HasOne]
- [HasManyThrough]
- [HasOneThrough]
- [Choosing Between BelongsTo and HasOne]
- [Self Joins]
- [Associations and the Database Schema]
@ -31,6 +34,7 @@ GRDB Associations
- [Available Association Aggregates]
- [Annotating a Request with Aggregates]
- [Filtering a Request with Aggregates]
- [Aggregate Operations]
- [Isolation of Multiple Aggregates]
- [DerivableRequest Protocol]
- [Known Issues]
@ -44,12 +48,12 @@ GRDB Associations
Associations streamline common operations in your code, make them safer, and more efficient. For example, consider a library application that has two record types, author and book:
```swift
struct Author: TableRecord, FetchableRecord {
struct Author {
var id: Int64
var name: String
}
struct Book: TableRecord, FetchableRecord {
struct Book {
var id: Int64
var authorId: Int64?
var title: String
@ -119,22 +123,68 @@ let bookInfos = BookInfo.fetchAll(db, request)
Before we dive in, please remember that associations can not generate all possible SQL queries that involve several tables. You may also *prefer* writing SQL, and this is just OK, because your SQL skills are welcome: see the [Joined Queries Support](../README.md#joined-queries-support) chapter.
## Required Protocols
**Associations are available on types that adopt the necessary supporting protocols.**
When your type is a subclass of the [Record class], all necessary protocols are already setup and ready.
Otherwise:
- **[TableRecord]** is the protocol that lets you declare associations between record types:
```swift
extension Author: TableRecord {
static let books = hasMany(Book.self)
}
extension Book: TableRecord {
static let author = belongsTo(Author.self)
}
```
- **[EncodableRecord]** makes it possible to use the `request(for:)` method, as below:
```swift
extension Book: EncodableRecord {
// The request which fetches the book's author.
var author: QueryInterfaceRequest<Author> {
return request(for: Book.author)
}
}
// Let's fetch a book's author:
try dbQueue.read { db in
let book: Book = ...
let author = try book.author.fetchOne(db) // Author?
}
```
A record type often conforms to EncodableRecord via the [PersistableRecord] protocol. However, PersistableRecord grants [persistence methods], the ones that are able to insert, update, and delete rows in the database. When you'd rather keep a record type read-only, and yet profit from associations, all you need is EncodableRecord.
EncodableRecord conformance can be derived from the standard Encodable protocol. See [Codable Records] for more information.
The Types of Associations
=========================
GRDB handles three types of associations:
GRDB handles five types of associations:
- **BelongsTo**
- **HasOne**
- **HasMany**
- **HasOne**
- **HasManyThrough**
- **HasOneThrough**
An association declares a link from a record type to another, as in "one book **belongs to** its author". It instructs GRDB to use the foreign keys declared in the database as support for Swift methods.
Each one of the three types of associations is appropriate for a particular database situation.
- [BelongsTo]
- [HasOne]
- [HasMany]
- [HasOne]
- [HasManyThrough]
- [HasOneThrough]
- [Choosing Between BelongsTo and HasOne]
- [Self Joins]
@ -146,7 +196,7 @@ The **BelongsTo** association sets up a one-to-one connection from a record type
For example, if your application includes authors and books, and each book is assigned its author, you'd declare the `Book.author` association as below, with its companion property:
```swift
struct Book: TableRecord {
struct Book: TableRecord, EncodableRecord {
static let author = belongsTo(Author.self)
var author: QueryInterfaceRequest<Author> {
return request(for: Book.author)
@ -159,7 +209,7 @@ struct Author: TableRecord {
}
```
The `Book.author` association will help you build [association requests]. The property lets you fetch a book's author:
The static `Book.author` association will help you build [association requests]. The property lets you fetch a book's author:
```swift
let book: Book = ...
@ -173,14 +223,47 @@ The **BelongsTo** association between a book and its author needs that the datab
See [Convention for the BelongsTo Association] for some sample code that defines the database schema for such an association.
## HasMany
The **HasMany** association indicates a one-to-many connection between two record types, such as each instance of the declaring record "has many" instances of the other record. You'll often find this association on the other side of a **BelongsTo** association.
For example, if your application includes authors and books, and each author is assigned zero or more books, you'd declare the `Author.books` association as below, with its companion property:
```swift
struct Author: TableRecord, EncodableRecord {
static let books = hasMany(Book.self)
var books: QueryInterfaceRequest<Book> {
return request(for: Author.books)
}
}
struct Book: TableRecord {
...
}
```
The static `Author.books` association will help you build [association requests]. The property lets you fetch an author's books:
```swift
let author: Author = ...
let books = try author.books.fetchAll(db) // [Book]
```
The **HasMany** association between an author and its books needs that the database table for books has a column that points to the table for authors:
![HasManySchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasManySchema.svg)
See [Convention for the HasMany Association] for some sample code that defines the database schema for such an association.
## HasOne
The **HasOne** association also sets up a one-to-one connection from a record type to another record type, but with different semantics, and underlying database schema. It is usually used when an entity has been denormalized into two database tables.
The **HasOne** association, like BelongsTo, sets up a one-to-one connection from a record type to another record type, but with different semantics, and underlying database schema. It is usually used when an entity has been denormalized into two database tables.
For example, if your application has one database table for countries, and another for their demographic profiles, you'd declare the `Country.demographics` association as below, with its companion property:
```swift
struct Country: TableRecord {
struct Country: TableRecord, EncodableRecord {
static let demographics = hasOne(Demographics.self)
var demographics: QueryInterfaceRequest<Demographics> {
return request(for: Country.demographics)
@ -193,7 +276,7 @@ struct Demographics: TableRecord {
}
```
The `Country.demographics` association will help you build [association requests]. The property lets you fetch a country's demographic profile:
The static `Country.demographics` association will help you build [association requests]. The property lets you fetch a country's demographic profile:
```swift
let country: Country = ...
@ -207,37 +290,109 @@ The **HasOne** association between a country and its demographics needs that the
See [Convention for the HasOne Association] for some sample code that defines the database schema for such an association.
## HasMany
## HasManyThrough
The **HasMany** association indicates a one-to-many connection between two record types, such as each instance of the declaring record "has many" instances of the other record. You'll often find this association on the other side of a **BelongsTo** association.
For example, if your application includes authors and books, and each author is assigned zero or more books, you'd declare the `Author.books` association as below, with its companion property:
The **HasManyThrough** association is often used to set up a many-to-many connection with another record. This association indicates that the declaring record can be matched with zero or more instances of another record by proceeding through a third record. For example, consider the practice of passport delivery. The relevant association declarations could look like this:
```swift
struct Author: TableRecord {
static let books = hasMany(Book.self)
var books: QueryInterfaceRequest<Book> {
return request(for: Author.books)
struct Country: TableRecord, EncodableRecord {
static let passports = hasMany(Passport.self)
static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen)
var citizens: QueryInterfaceRequest<Citizen> {
return request(for: Country.citizens)
}
}
struct Book: TableRecord {
...
struct Passport: TableRecord {
static let country = belongsTo(Country.self)
static let citizen = belongsTo(Citizen.self)
}
struct Citizen: TableRecord, EncodableRecord {
static let passports = hasMany(Passport.self)
static let countries = hasMany(Country.self, through: passports, using: Passport.country)
var countries: QueryInterfaceRequest<Country> {
return request(for: Citizen.countries)
}
}
```
The `Author.books` association will help you build [association requests]. The property lets you fetch an author's books:
![HasManyThroughSchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasManyThroughSchema.svg)
The static `Country.citizens` association will help you build [association requests]. The property lets you fetch a country's citizens:
```swift
let author: Author = ...
let books = try author.books.fetchAll(db) // [Book]
let country: Country = ...
let citizens = try country.citizens.fetchAll(db) // [Citizen]
```
The **HasMany** association between an author and its books needs that the database table for books has a column that points to the table for authors:
The **HasManyThrough** association is also useful for setting up "shortcuts" through nested HasMany associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. You could set that up this way:
![HasManySchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasManySchema.svg)
```swift
struct Document: TableRecord {
static let sections = hasMany(Section.self)
static let paragraphs = hasMany(Paragraph.self, through: sections, using: Section.paragraphs)
}
See [Convention for the HasMany Association] for some sample code that defines the database schema for such an association.
struct Section: TableRecord {
static let paragraphs = hasMany(Paragraph.self)
}
struct Paragraph: TableRecord {
}
```
As in the examples above, **HasManyThrough** association is always built from two other associations: the `through:` and `using:` arguments. Those associations can be any other association (BelongsTo, HasMany, HasManyThrough, etc). The above `Document.paragraphs` association can also be defined, in a much more explicit way, as below:
```swift
struct Document: TableRecord {
static let paragraphs = hasMany(
Paragraph.self,
through: Document.hasMany(Section.self),
using: Section.hasMany(Paragraph.self))
}
```
## HasOneThrough
A **HasOneThrough** association sets up a one-to-one connection with another record. This association indicates that the declaring record can be matched with one instance of another record by proceeding through a third record. For example, if each book belongs to a library, and each library has one address, then one knows where the book should be returned to:
```swift
struct Book: TableRecord, EncodableRecord {
static let library = belongsTo(Library.self)
static let returnAddress = hasOne(Address.self, through: library, using: library.address)
var returnAddress: QueryInterfaceRequest<Address> {
return request(for: Book.returnAddress)
}
}
struct Library: TableRecord {
static let address = hasOne(Address.self)
}
struct Address: TableRecord {
}
```
![HasOneThroughSchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasOneThroughSchema.svg)
The static `Book.returnAddress` association will help you build [association requests]. The property lets you fetch a book's return address:
```swift
let book: Book = ...
let address = try book.returnAddress.fetchOne(db) // Address?
```
As in the example above, **HasOneThrough** association is always built from two other associations: the `through:` and `using:` arguments. Those associations can be any other association to one (BelongsTo, HasOne, HasOneThrough). The above `Book.returnAddress` association can also be defined, in a much more explicit way, as below:
```swift
struct Book: TableRecord {
static let returnAddress = hasOne(
Address.self,
through: Book.belongsTo(Library.self),
using: Library.hasOne(Address.self))
}
```
## Choosing Between BelongsTo and HasOne
@ -303,8 +458,8 @@ Those conventions help associations be convenient and, generally, "just work". W
- [Convention for Database Table Names]
- [Convention for the BelongsTo Association]
- [Convention for the HasOne Association]
- [Convention for the HasMany Association]
- [Convention for the HasOne Association]
- [Foreign Keys]
@ -362,13 +517,18 @@ See [The Structure of a Joined Request] for more information.
## Convention for the BelongsTo Association
**[BelongsTo] associations should be supported by an SQLite foreign key.**
Foreign keys are the recommended way to declare relationships between database tables. Not only will SQLite guarantee the integrity of your data, but GRDB will be able to use those foreign keys to automatically configure your associations.
```swift
extension Book: TableRecord, EncodableRecord {
static let author = belongsTo(Author.self)
var author: QueryInterfaceRequest<Author> {
return request(for: Book.author)
}
}
```
![BelongsToSchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/BelongsToSchema.svg)
The matching [migration] could look like:
Here is the recommended [migration] for the **[BelongsTo]** association:
```swift
migrator.registerMigration("Books and Authors") { db in
@ -417,15 +577,82 @@ struct Book: FetchableRecord, TableRecord {
See [Foreign Keys] for more information.
## Convention for the HasMany Association
```swift
extension Author: TableRecord, EncodableRecord {
static let books = hasMany(Book.self)
var books: QueryInterfaceRequest<Book> {
return request(for: Author.books)
}
}
```
![HasManySchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasManySchema.svg)
Here is the recommended [migration] for the **[HasMany]** association:
```swift
migrator.registerMigration("Books and Authors") { db in
try db.create(table: "author") { t in
t.autoIncrementedPrimaryKey("id") // (1)
t.column("name", .text)
}
try db.create(table: "book") { t in
t.autoIncrementedPrimaryKey("id")
t.column("authorId", .integer) // (2)
.notNull() // (3)
.indexed() // (4)
.references("author", onDelete: .cascade) // (5)
t.column("title", .text)
}
}
```
1. The `author` table has a primary key.
2. The `book.authorId` column is used to link a book to the author it belongs to.
3. Make the `book.authorId` column not null if you want SQLite to guarantee that all books have an author.
4. Create an index on the `book.authorId` column in order to ease the selection of an author's books.
5. Create a foreign key from `book.authorId` column to `authors.id`, so that SQLite guarantees that no book refers to a missing author. The `onDelete: .cascade` option has SQLite automatically delete all of an author's books when that author is deleted. See [Foreign Key Actions] for more information.
The example above uses auto-incremented primary keys. But generally speaking, all primary keys are supported.
Following this convention lets you write, for example:
```swift
struct Book: FetchableRecord, TableRecord {
}
struct Author: FetchableRecord, TableRecord {
static let books = hasMany(Book.self)
}
```
If the database schema does not follow this convention, and does not define foreign keys between tables, you can still use **HasMany** associations. But your help is needed to define the missing foreign key:
```swift
struct Author: FetchableRecord, TableRecord {
static let books = hasMany(Book.self, using: ForeignKey(...))
}
```
See [Foreign Keys] for more information.
## Convention for the HasOne Association
**[HasOne] associations should be supported by an SQLite foreign key.**
Foreign keys are the recommended way to declare relationships between database tables. Not only will SQLite guarantee the integrity of your data, but GRDB will be able to use those foreign keys to automatically configure your associations.
```swift
extension Country: TableRecord, EncodableRecord {
static let demographics = hasOne(Demographics.self)
var demographics: QueryInterfaceRequest<Demographics> {
return request(for: Country.demographics)
}
}
```
![HasOneSchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasOneSchema.svg)
The matching [migration] could look like:
Here is the recommended [migration] for the **[HasOne]** association:
```swift
migrator.registerMigration("Countries") { db in
@ -475,63 +702,6 @@ struct Country: FetchableRecord, TableRecord {
See [Foreign Keys] for more information.
## Convention for the HasMany Association
**[HasMany] associations should be supported by an SQLite foreign key.**
Foreign keys are the recommended way to declare relationships between database tables. Not only will SQLite guarantee the integrity of your data, but GRDB will be able to use those foreign keys to automatically configure your associations.
![HasManySchema](https://cdn.rawgit.com/groue/GRDB.swift/master/Documentation/Images/Associations2/HasManySchema.svg)
The matching [migration] could look like:
```swift
migrator.registerMigration("Books and Authors") { db in
try db.create(table: "author") { t in
t.autoIncrementedPrimaryKey("id") // (1)
t.column("name", .text)
}
try db.create(table: "book") { t in
t.autoIncrementedPrimaryKey("id")
t.column("authorId", .integer) // (2)
.notNull() // (3)
.indexed() // (4)
.references("author", onDelete: .cascade) // (5)
t.column("title", .text)
}
}
```
1. The `author` table has a primary key.
2. The `book.authorId` column is used to link a book to the author it belongs to.
3. Make the `book.authorId` column not null if you want SQLite to guarantee that all books have an author.
4. Create an index on the `book.authorId` column in order to ease the selection of an author's books.
5. Create a foreign key from `book.authorId` column to `authors.id`, so that SQLite guarantees that no book refers to a missing author. The `onDelete: .cascade` option has SQLite automatically delete all of an author's books when that author is deleted. See [Foreign Key Actions] for more information.
The example above uses auto-incremented primary keys. But generally speaking, all primary keys are supported.
Following this convention lets you write, for example:
```swift
struct Book: FetchableRecord, TableRecord {
}
struct Author: FetchableRecord, TableRecord {
static let books = hasMany(Book.self)
}
```
If the database schema does not follow this convention, and does not define foreign keys between tables, you can still use **HasMany** associations. But your help is needed to define the missing foreign key:
```swift
struct Author: FetchableRecord, TableRecord {
static let books = hasMany(Book.self, using: ForeignKey(...))
}
```
See [Foreign Keys] for more information.
## Foreign Keys
**Associations can automatically infer the foreign keys that define how two database tables are linked together.**
@ -630,7 +800,7 @@ Fetch requests do not visit the database until you fetch values from them. This
For example, given a `Book.author` **[BelongsTo]** association, you can build a request for the author of a book. In the example below, we return this request from the `Book.author` property:
```swift
struct Book: PersistableRecord {
struct Book: TableRecord, EncodableRecord {
static let author = belongsTo(Author.self)
/// The request for a book's author
@ -647,10 +817,10 @@ let book: Book = ...
let author = try book.author.fetchOne(db) // Author?
```
**[HasOne]** and **[HasMany]** associations can also build requests for associated records. For example:
**[HasOne]**, **[HasMany]**, **[HasOneThrough]**, and **[HasManyThrough]** associations can also build requests for associated records. For example:
```swift
struct Author: PersistableRecord {
struct Author: TableRecord, EncodableRecord {
static let books = hasMany(Book.self)
/// The request for an author's books
@ -797,7 +967,7 @@ let request = Book
The request above fetches all books, along with their author, and their author's country.
When you chain associations, you can avoid fetching intermediate values by replacing the `including` method with `joining`:
When you chain associations, you can avoid fetching intermediate tables by replacing the `including` method with `joining`. The request below fetches all books, along with their author's country, but does not include the intermediate authors in the fetched results:
```swift
// SELECT book.*, country.*
@ -809,7 +979,15 @@ let request = Book
.including(optional: Person.country))
```
The request above fetches all books, along with their author's country.
**[HasOneThrough]** and **[HasManyThrough]** associations provide a shortcut for those requests that skip intermediate tables:
```swift
// SELECT book.*, country.*
// FROM book
// LEFT JOIN person ON person.id = book.authorId
// LEFT JOIN country ON country.code = person.countryCode
let request = Book.including(optional: Book.country)
```
> :warning: **Warning**: you can not currently chain a required association behind an optional association:
>
@ -1689,6 +1867,91 @@ The `having(_:)` method filters a request according to an aggregated value. You
```
### Aggregate Operations
Aggregates can be modified and combined with Swift operators:
- Logical operators `&&`, `||` and `!`
<details>
<summary>SQL</summary>
```sql
SELECT author.*
FROM author
LEFT JOIN book ON book.authorId = author.id
LEFT JOIN painting ON painting.authorId = author.id
GROUP BY author.id
HAVING ((COUNT(DISTINCT book.rowid) = 0) AND (COUNT(DISTINCT painting.rowid) = 0))
```
</details>
```swift
let condition = Author.books.isEmpty && Author.paintings.isEmpty
let request = Author.having(condition)
```
- Comparison operators `<`, `<=`, `=`, `!=`, `>=`, `>`
<details>
<summary>SQL</summary>
```sql
SELECT author.*
FROM author
LEFT JOIN book ON book.authorId = author.id
GROUP BY author.id
HAVING MAX(book.year) >= 2010
```
</details>
```swift
let request = Author.having(Author.books.max(Column("year")) >= 2010)
```
- Arithmetic operators `+`, `-`, `*`, `/`
<details>
<summary>SQL</summary>
```sql
SELECT author.*,
(COUNT(DISTINCT book.rowid) +
COUNT(DISTINCT painting.rowid)) AS workCount
FROM author
LEFT JOIN book ON book.authorId = author.id
LEFT JOIN painting ON painting.authorId = author.id
GROUP BY author.id
```
</details>
```swift
let workCount = Author.books.count + Author.paintings.count)
let request = Author.annotated(with: workCount.aliased("workCount"))
```
- IFNULL operator `??`
<details>
<summary>SQL</summary>
```sql
SELECT "team".*, IFNULL(MIN("player"."score"), 0) AS "minPlayerScore"
FROM "team"
LEFT JOIN "player" ON ("player"."teamId" = "team"."id")
GROUP BY "team"."id"
```
</details>
```swift
let request = Team.annotated(with: Team.players.min(Column("score")) ?? 0)
```
### Isolation of Multiple Aggregates
When you compute multiple aggregates, make sure they use as many distinct **[association keys](#the-structure-of-a-joined-request)** as there are distinct populations of associated records.
@ -1894,9 +2157,7 @@ The APIs that have been described above do not cover the whole topic of joined r
- One can not yet express requests such as "all authors with all their books".
- There's no HasOneThrough and HasManyThrough association, which would allow to skip intermediate bridge records when building requests.
Those features are not present yet because they hide several very tough challenges. Come [discuss](http://twitter.com/groue) for more information, or if you wish to help turning those features into reality.
Come [discuss](http://twitter.com/groue) for more information, or if you wish to help turning those features into reality.
---
@ -1941,9 +2202,12 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[Associations Benefits]: #associations-benefits
[Required Protocols]: #required-protocols
[BelongsTo]: #belongsto
[HasOne]: #hasone
[HasMany]: #hasmany
[HasOne]: #hasone
[HasManyThrough]: #hasmanythrough
[HasOneThrough]: #hasonethrough
[Choosing Between BelongsTo and HasOne]: #choosing-between-belongsto-and-hasone
[Self Joins]: #self-joins
[The Types of Associations]: #the-types-of-associations
@ -1976,6 +2240,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[Available Association Aggregates]: #available-association-aggregates
[Annotating a Request with Aggregates]: #annotating-a-request-with-aggregates
[Filtering a Request with Aggregates]: #filtering-a-request-with-aggregates
[Aggregate Operations]: #aggregate-operations
[Isolation of Multiple Aggregates]: #isolation-of-multiple-aggregates
[DerivableRequest Protocol]: #derivablerequest-protocol
[Known Issues]: #known-issues
@ -1986,3 +2251,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[association requests]: #building-requests-from-associations
[Good Practices for Designing Record Types]: GoodPracticesForDesigningRecordTypes.md
[Fetching Aggregated Values]: ../README.md#fetching-aggregated-values
[Record class]: ../README.md#record-class
[EncodableRecord]: ../README.md#persistablerecord-protocol
[PersistableRecord]: ../README.md#persistablerecord-protocol
[Codable Records]: ../README.md#codable-records
[persistence methods]: ../README.md#persistence-methods

View File

@ -81,7 +81,7 @@ try db.create(virtualTable: "documents", using: FTS5()) { t in
The full-text table can be fed and queried in [a regular way](../../../#full-text-search):
```swift
try db.execute("INSERT INTO documents VALUES (?)", arguments: ["..."])
try db.execute(sql: "INSERT INTO documents VALUES (?)", arguments: ["..."])
try Document(content: "...").insert(db)
let pattern = FTS5Pattern(matchingAnyTokenIn:"...")

View File

@ -0,0 +1,414 @@
SQL Interpolation
=================
Your SQL skills are [welcomed] throughout GRDB. Yet writing raw SQL presents commonplace challenges. For example, you want to make sure your queries don't break whenever the database schema changes as you ship new versions of your application. When you inject user values in the database, you have to use statement arguments, and it is easy to make a mistake in the process.
The query below exemplifies this situation. It even contains a bug that is not quite easy to spot:
```swift
try db.execute(
sql: """
UPDATE student
SET firstName = ?, lastName = ?, department = ?, birthDate = ?,
registrationDate = ?, mainTeacherId = ?
WHERE id = ?
""",
arguments: [firstName, lastName, department, birthDate,
registrationDate, mainTeacherId])
```
SQL Interpolation is an answer to these troubles. It is available in Swift 5.
- [Introduction]
- [SQLLiteral]
- [SQL Interpolation and Record Protocols]
- [SQL Interpolation Reference]
## Introduction
**SQL Interpolation** lets you embed values in your SQL queries by wrapping them inside `\(` and `)`:
```swift
let name: String = ...
let id: Int64 = ...
try db.execute(literal: "UPDATE player SET name = \(name) WHERE id = \(id)")
```
SQL interpolation looks and feel just like regular [String interpolation]:
```swift
let name = "World"
print("Hello \(name)!") // prints "Hello World!"
```
The difference is that it generates valid SQL which does not suffer from syntax errors or [SQL injection]. For example, you do not need to validate input or process single quotes:
```swift
// Executes `UPDATE player SET name = 'O''Brien' WHERE id = 42`
let name = "O'Brien"
let id = 42
try db.execute(literal: "UPDATE player SET name = \(name) WHERE id = \(id)")
```
Under the hood, SQL interpolation generates a plain SQL string. It runs exactly as below:
```swift
try db.execute(sql: "UPDATE player SET name = ? WHERE id = ?", arguments: [name, id])
```
Plain SQL strings are indeed still available, and SQL interpolation only kicks in when you ask for it. There is a simple rule to remember:
- For plain SQL strings, use the `sql` argument label:
```swift
try db.execute(sql: "UPDATE player SET name = ? WHERE id = ?", arguments: [name, id])
```
- For SQL interpolation, use the `literal` argument label:
```swift
try db.execute(literal: "UPDATE player SET name = \(name) WHERE id = \(id)")
```
## SQLLiteral
**SQLLiteral** is the type that looks like a plain String, but profits from SQL interpolation:
```swift
let query: SQLLiteral = "UPDATE player SET name = \(name) WHERE id = \(id)"
try db.execute(literal: query)
```
**SQLLiteral is not a Swift String.** You can not use the `execute(literal:)` method with a String argument:
```swift
// Compiler error:
// Cannot convert value of type 'String' to expected argument type 'SQLLiteral'
let query = "UPDATE player SET name = \(name) WHERE id = \(id)" // a String
try db.execute(literal: query)
```
SQLLiteral can build your queries step by step, with regular operators and methods:
```swift
// +, +=, append
var query: SQLLiteral = "UPDATE player "
query += "SET name = \(name) "
query.append(literal: "WHERE id = \(id)")
// joined(), joined(separator:)
let components: [SQLLiteral] = [
"UPDATE player",
"SET name = \(name)",
"WHERE id = \(id)"
]
let query = components.joined(separator: " ")
```
Extract the plain SQL string from a literal:
```swift
let query: SQLLiteral = "UPDATE player SET name = \(name) WHERE id = \(id)"
print(query.sql) // prints "UPDATE player SET name = ? WHERE id = ?"
print(query.arguments) // prints ["O'Brien", 42]
```
Build a literal from a plain SQL string:
```swift
let query = SQLLiteral(
sql: "UPDATE player SET name = ? WHERE id = ?",
arguments: [name, id])
```
SQLLiteral can embed any [value], as we have seen above, but not only. Please keep on reading the next chapter, or jump directly to the [SQL Interpolation Reference].
## SQL Interpolation and Record Protocols
The [record protocols] extend your application types with database abilities.
**A record type knows everything about the schema of its underlying database table**. With the [TableRecord] protocol, the `databaseTableName` property contains the table name. With the [Decodable] protocol, the [CodingKeys] enum contain the column names. And with [FetchableRecord], you can decode raw database rows.
SQL Interpolation puts this knowledge to good use, so that you can build robust queries that consistently use correct table and column names:
```swift
struct Player {
var id: Int64
var name: String
var score: Int?
}
extension Player: Decodable, FetchableRecord, TableRecord {
/// Deletes all player with no score
static func deleteAllWithoutScore(_ db: Database) throws {
try db.execute(literal: "DELETE FROM \(self) WHERE \(CodingKeys.score) IS NULL")
}
/// The player with a given id
static func filter(id: Int64) -> SQLRequest<Player> {
return "SELECT * FROM \(self) WHERE \(CodingKeys.id) = \(id)"
}
/// All players with the given ids
static func filter(ids: [Int64]) -> SQLRequest<Player> {
return "SELECT * FROM \(self) WHERE \(CodingKeys.id) IN \(ids)"
}
/// The maximum score
static func maximumScore() -> SQLRequest<Int> {
return "SELECT MAX(\(CodingKeys.score)) FROM \(self)"
}
/// All players whose score is the maximum score
static func leaders() -> SQLRequest<Player> {
return """
SELECT * FROM \(self)
WHERE \(CodingKeys.score) = \(maximumScore())
"""
}
/// A complex request
static func complexRequest() -> SQLRequest<Player> {
let query: SQLLiteral = "SELECT * FROM \(self) "
query += "JOIN \(Team.self) ON ..."
query += "GROUP BY ..."
return SQLRequest(literal: query)
}
}
```
Let's breakdown each one of those methods.
- `deleteAllWithoutScore(_:)`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// Deletes all player with no score
static func deleteAllWithoutScore(_ db: Database) throws {
try db.execute(literal: "DELETE FROM \(self) WHERE \(CodingKeys.score) IS NULL")
}
}
```
Usage:
```swift
try dbQueue.write { db in
// DELETE FROM player WHERE score IS NULL
try Player.deleteAllWithoutScore(db)
}
```
`DELETE FROM \(self) ...` embeds the Player type itself. Since Player adopts the [TableRecord] protocol, this embeds `Player.databaseTableName` in the SQL query.
`... \(CodingKeys.score) IS NULL` embeds CodingKeys.score. This one has been synthesized by the Swift compiler because Player adopts the Decodable protocol. It embeds the column name in the SQL query.
- `filter(id:)`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// The player with a given id
static func filter(id: Int64) -> SQLRequest<Player> {
return "SELECT * FROM \(self) WHERE \(CodingKeys.id) = \(id)"
}
}
```
Usage:
```swift
let player = try dbQueue.read { db in
// SELECT * player WHERE id = 42
try Player.filter(id: 42).fetchOne(db) // Player?
}
```
The return type of this method is `SQLRequest<Player>`. It is one of the GRDB [request types]. And it profits from SQL interpolation: this is why this method can simply return an "SQL literal"".
It embeds `\(self)` (the Player type which adopts the TableRecord protocol) and `\(CodingKeys.id)` (the coding key synthesized by the Decodable protocol), and `\(id)` (a [value]).
- `filter(ids:)`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// All players with the given ids
static func filter(ids: [Int64]) -> SQLRequest<Player> {
return "SELECT * FROM \(self) WHERE \(CodingKeys.id) IN \(ids)"
}
}
```
Usage:
```swift
let players = try dbQueue.read { db in
// SELECT * player WHERE id IN (1, 2, 3)
try Player.filter(ids: [1, 2, 3]).fetchAll(db) // [Player]
}
```
It embeds `\(ids)`, an array of ids. All [value] sequences are supported (arrays, sets, etc.) Empty sequences are supported as well, with both `IN` and `NOT IN` SQL operators.
- `maximumScore()`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// The maximum score
static func maximumScore() -> SQLRequest<Int> {
return "SELECT MAX(\(CodingKeys.score)) FROM \(self)"
}
}
```
Usage:
```swift
let maximumScore = try dbQueue.read { db in
// SELECT MAX(score) FROM player
try Player.maximumScore().fetchOne(db) // Int?
}
```
The result is `SQLRequest<Int>`, unlike previous requests of type `SQLRequest<Player>`. SQLRequest accepts any fetchable type (database [row], simple [value], or custom [record]).
- `leaders()`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// All players whose score is the maximum score
static func leaders() -> SQLRequest<Player> {
return """
SELECT * FROM \(self)
WHERE \(CodingKeys.score) = \(maximumScore())
"""
}
}
```
Usage:
```swift
let leaders = try dbQueue.read { db in
// SELECT * FROM player
// WHERE score = (SELECT MAX(score) FROM player)
try Player.leaders().fetchAll(db) // [Player]
}
```
This request embeds `\(maximumScore())`, the `SQLRequest<Int>` returned by the `maximumScore` method. After values, coding keys, and types that adopt the TableRecord protocol, this ends our quick tour of things you can embed between `\(` and `)`. Check out the [SQL Interpolation Reference] for the full list of supported interpolations.
- `complexRequest()`
```swift
extension Player: Decodable, FetchableRecord, TableRecord {
/// A complex request
static func complexRequest() -> SQLRequest<Player> {
let query: SQLLiteral = "SELECT * FROM \(self) "
query += "JOIN \(Team.self) ON ..."
query += "GROUP BY ..."
return SQLRequest(literal: query)
}
}
```
This last request shows how to build an SQLRequest from an [SQLLiteral]. You will need SQLLiteral when the request can not be expressed in a single "SQL literal".
## SQL Interpolation Reference
This chapter lists all kinds of supported interpolations.
- Types adopting the [TableRecord] protocol:
```swift
// SELECT * FROM player
extension Player: TableRecord { }
"SELECT * FROM \(Player.self)"
```
- [Expressions] and [values]:
```swift
// SELECT name FROM player
"SELECT \(Column("name")) FROM player"
// SELECT (score + 100) AS points FROM player
let bonus = 100
"SELECT \(Column("score") + bonus) AS points FROM player"
// SELECT (score + 100) AS points FROM player
"SELECT (score + \(bonus)) AS points FROM player"
```
- Coding keys:
```swift
// SELECT name FROM player
"SELECT \(CodingKeys.name) FROM player"
```
- Sequences:
```swift
// SELECT * FROM player WHERE id IN (1, 2, 3)
let ids = [1, 2, 3]
"SELECT * FROM player WHERE id IN \(ids)"
```
- Orderings:
```swift
// SELECT * FROM player ORDER BY name DESC
"SELECT * FROM player WHERE id IN \(Column("name").desc)"
```
- SQLRequest:
```swift
// SELECT * FROM player WHERE score = (SELECT MAX(score) FROM player)
let subQuery: SQLRequest<Int> = "SELECT MAX(score) FROM player"
"SELECT * FROM player WHERE score = \(subQuery)"
```
- SQLLiteral:
```swift
// SELECT * FROM player WHERE name = 'O''Brien'
let condition: SQLLiteral = "name = \("O'Brien")"
"SELECT * FROM player WHERE \(literal: condition)"
```
- Plain SQL strings and eventual arguments:
```swift
// SELECT * FROM player
"SELECT * FROM \(sql: "player")"
// SELECT * FROM player WHERE name = 'O''Brien'
"SELECT * FROM player WHERE \(sql: "name = ?", arguments: ["O'Brien"])"
```
[Introduction]: #introduction
[SQLLiteral]: #sqlliteral
[SQL Interpolation and Record Protocols]: #sql-interpolation-and-record-protocols
[SQL Interpolation Reference]: #sql-interpolation-reference
[String interpolation]: https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html#ID292
[SQL injection]: ../README.md#avoiding-sql-injection
[record protocols]: ../README.md#record-protocols-overview
[FetchableRecord]: ../README.md#fetchablerecord-protocol
[TableRecord]: ../README.md#tablerecord-protocol
[Decodable]: ../README.md#codable-records
[CodingKeys]: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
[value]: ../README.md#values
[values]: ../README.md#values
[request types]: ../README.md#custom-requests
[row]: ../README.md#row-queries
[record]: ../README.md#records
[Expressions]: ../README.md#expressions
[welcomed]: ../README.md#sqlite-api
[SE-0228 Fix ExpressibleByStringInterpolation]: https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md

View File

@ -69,7 +69,7 @@ By adopting the [FetchableRecord] protocol, places can be loaded from SQL reques
```swift
extension Place: FetchableRecord { ... }
let places = try Place.fetchAll(db, "SELECT * FROM place") // [Place]
let places = try Place.fetchAll(db, sql: "SELECT * FROM place") // [Place]
```
Add the [TableRecord] protocol, and SQL requests are generated for you:
@ -177,9 +177,14 @@ For further information about GRDB concurrency, check its detailed [Concurrency
SQL is a weird language. Born in the 70s, easy to [misuse](https://xkcd.com/327/), feared by some developers, despised by others, and yet wonderfully concise and powerful.
GRDB [query interface] and [associations] can generate SQL for you:
GRDB [records], [query interface] and [associations] can generate SQL for you:
```swift
// UPDATE player SET score = 950 WHERE id = 42
try player.updateChanges {
$0.score += 10
}
// SELECT * FROM player ORDER BY score DESC LIMIT 10
let bestPlayers: [Player] = try Player
.order(scoreColumn.desc)
@ -202,19 +207,47 @@ let bookInfos: [BookInfo] = BookInfo.fetchAll(db, request)
But you can always switch to SQL when you want to:
```swift
let bestPlayers: [Player] = try Player.fetchAll(db, """
try db.execute(
sql: "UPDATE player SET score = ? WHERE id = ?",
arguments: [950, 42])
let bestPlayers: [Player] = try Player.fetchAll(db, sql: """
SELECT * FROM player ORDER BY score DESC LIMIT 10
""")
let maximumScore: Int? = try Int.fetchOne(db, """
let maximumScore: Int? = try Int.fetchOne(db, sql: """
SELECT MAX(score) FROM player
""")
```
With Swift 5, you can profit from **SQL interpolation**. It lets you build SQL queries from natural looking strings, but without any risk of syntax error or [SQL injection](https://xkcd.com/327/):
```swift
try db.execute(literal: "UPDATE player SET score = \(score) WHERE id = \(id)")
extension Player {
static func filter(name: String) -> SQLRequest<Player> {
return "SELECT * FROM \(self) WHERE \(CodingKeys.name) = \(name)"
}
}
let player = try Player.filter(name: "Arthur O'Brien").fetchOne(db)
```
Custom SQL requests as the one above are welcome in database observation tools like [ValueObservation] and [RxGRDB]:
```swift
Player.filter(name: "Arthur O'Brien").rx
.fetchOne(in: dbQueue)
.subscribe(onNext: { (player: Player?) in
print("Player has changed")
})
```
Power users can also consume complex joined SQL queries into handy record values, by *adapting* raw database rows to the decoded records (see [Joined Queries Support](#../README.md#joined-queries-support)):
```swift
let bookInfos: [BookInfo] = SQLRequest<BookInfo>("""
let bookInfos: [BookInfo] = SQLRequest<BookInfo>(sql: """
SELECT book.*, author.*
FROM book
LEFT JOIN author ON author.id = book.authorId
@ -232,7 +265,7 @@ let bookInfos: [BookInfo] = SQLRequest<BookInfo>("""
In performance-critical sections, you may want to deal with raw database rows, and fetch [lazy cursors](../README.md#cursors) instead of arrays:
```swift
let rows = try Row.fetchCursor(db, "SELECT id, name, score FROM player")
let rows = try Row.fetchCursor(db, sql: "SELECT id, name, score FROM player")
while let row = try rows.next() {
let id: Int64 = row[0]
let name: String = row[1]
@ -240,28 +273,6 @@ while let row = try rows.next() {
}
```
When you feel like your code clarity would be enhanced by hiding your custom SQL in a dedicated method, you can build [custom requests](../README.md#custom-requests):
```swift
extension Player {
static func customRequest(...) -> SQLRequest<Player> {
return SQLRequest<Player>("SELECT ...", arguments: ...)
}
}
let players = try Player.customRequest(...).fetchAll(db)
```
Those custom requests are welcome in database observation tools like [ValueObservation] and [RxGRDB]:
```swift
Player.customRequest(...).rx
.fetchAll(in: dbQueue)
.subscribe(onNext: { players: [Player] in
print("Players have changed")
})
```
---
If this little tour of GRDB has convinced you, the real trip starts here: [GRDB].
@ -294,3 +305,4 @@ Happy GRDB! :gift:
[query interface]: ../README.md#the-query-interface
[associations]: AssociationsBasics.md
[Codable records]: ../README.md#codable-records
[records]: ../README.md#records

View File

@ -9,7 +9,7 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/groue/GRDB.swift.git', :tag => "v#{s.version}" }
s.module_name = 'GRDB'
s.ios.deployment_target = '8.0'
s.ios.deployment_target = '9.0'
s.osx.deployment_target = '10.9'
s.watchos.deployment_target = '2.0'

View File

@ -70,10 +70,21 @@
5613ED5621A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */; };
5613EDA621A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */; };
5613EDA721A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */; };
5615B261222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B25A222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift */; };
5615B262222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B25A222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift */; };
5615B26A222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B269222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift */; };
5615B26B222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B269222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift */; };
5615B275222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B274222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift */; };
5615B276222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B274222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift */; };
5615B288222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B287222B17BF00061C1C /* AssociationHasOneThroughDecodableRecordTests.swift */; };
5615B289222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5615B287222B17BF00061C1C /* AssociationHasOneThroughDecodableRecordTests.swift */; };
561667051D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; };
5616AAF1207CD45E00AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */; };
5616AAF2207CD45E00AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */; };
5616AAF3207CD45E00AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */; };
5617294E223533F40006E219 /* EncodableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56172947223533F40006E219 /* EncodableRecord.swift */; };
5617294F223533F40006E219 /* EncodableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56172947223533F40006E219 /* EncodableRecord.swift */; };
56172950223533F40006E219 /* EncodableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56172947223533F40006E219 /* EncodableRecord.swift */; };
56176C591EACCCC7000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AD001DAA8ACA0056AF8C /* FTS5CustomTokenizerTests.swift */; };
56176C5A1EACCCC7000F3F2B /* FTS5PatternTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B964C01DA521450002DA19 /* FTS5PatternTests.swift */; };
56176C5B1EACCCC7000F3F2B /* FTS5RecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B964C11DA521450002DA19 /* FTS5RecordTests.swift */; };
@ -415,9 +426,9 @@
5674A6EB1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6E21F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift */; };
5674A6EF1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6E21F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift */; };
5674A6F01F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6E21F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift */; };
5674A6F41F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* PersistableRecord+Encodable.swift */; };
5674A6F81F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* PersistableRecord+Encodable.swift */; };
5674A6F91F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* PersistableRecord+Encodable.swift */; };
5674A6F41F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* EncodableRecord+Encodable.swift */; };
5674A6F81F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* EncodableRecord+Encodable.swift */; };
5674A6F91F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F31F307F600095F066 /* EncodableRecord+Encodable.swift */; };
5674A6FB1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F21F307F600095F066 /* FetchableRecord+Decodable.swift */; };
5674A6FF1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F21F307F600095F066 /* FetchableRecord+Decodable.swift */; };
5674A7001F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674A6F21F307F600095F066 /* FetchableRecord+Decodable.swift */; };
@ -438,14 +449,16 @@
567DAF221EAB61ED00FC0928 /* grdb_config.h in Headers */ = {isa = PBXBuildFile; fileRef = 567DAF141EAB61ED00FC0928 /* grdb_config.h */; settings = {ATTRIBUTES = (Private, ); }; };
567DAF351EAB789800FC0928 /* DatabaseLogErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DAF341EAB789800FC0928 /* DatabaseLogErrorTests.swift */; };
567DAF391EAB789800FC0928 /* DatabaseLogErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DAF341EAB789800FC0928 /* DatabaseLogErrorTests.swift */; };
567ECE4F2222E431009245CA /* GRDB-4.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567ECE4E2222E431009245CA /* GRDB-4.0.swift */; };
567ECE502222E431009245CA /* GRDB-4.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567ECE4E2222E431009245CA /* GRDB-4.0.swift */; };
567ECE512222E431009245CA /* GRDB-4.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567ECE4E2222E431009245CA /* GRDB-4.0.swift */; };
567F0B2D220F0E2E00D111FB /* SQLInterpolationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567F0B2C220F0E2E00D111FB /* SQLInterpolationTests.swift */; };
567F0B2E220F0E2E00D111FB /* SQLInterpolationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567F0B2C220F0E2E00D111FB /* SQLInterpolationTests.swift */; };
567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567F45A71F888B2600030B59 /* TruncateOptimizationTests.swift */; };
567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567F45A71F888B2600030B59 /* TruncateOptimizationTests.swift */; };
568068311EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */; };
568068351EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */; };
568735A21CEDE16C009B9116 /* Betty.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5687359E1CEDE16C009B9116 /* Betty.jpeg */; };
56873BEC1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; };
56873BEF1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; };
56873BF21F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; };
568D131F2207213E00674B58 /* SQLSelectQueryGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568D13182207213E00674B58 /* SQLSelectQueryGenerator.swift */; };
568D13202207213E00674B58 /* SQLSelectQueryGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568D13182207213E00674B58 /* SQLSelectQueryGenerator.swift */; };
568D13212207213F00674B58 /* SQLSelectQueryGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568D13182207213E00674B58 /* SQLSelectQueryGenerator.swift */; };
@ -471,6 +484,13 @@
5695312A1C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */; };
569531351C919DF200CF1A2B /* DatabasePoolCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */; };
569531381C919DF700CF1A2B /* DatabasePoolFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */; };
5695961C222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959615222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift */; };
5695961D222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959615222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift */; };
56959629222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959628222C462D002CB7C9 /* HasManyThroughAssociation.swift */; };
5695962A222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959628222C462D002CB7C9 /* HasManyThroughAssociation.swift */; };
5695962B222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959628222C462D002CB7C9 /* HasManyThroughAssociation.swift */; };
56959633222D056D002CB7C9 /* AssociationHasManySQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959632222D056D002CB7C9 /* AssociationHasManySQLTests.swift */; };
56959634222D056D002CB7C9 /* AssociationHasManySQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56959632222D056D002CB7C9 /* AssociationHasManySQLTests.swift */; };
5698AC031D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; };
5698AC071D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; };
5698AC371D9E5A590056AF8C /* FTS3Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC361D9E5A590056AF8C /* FTS3Pattern.swift */; };
@ -506,10 +526,10 @@
5698AD351DABAF4A0056AF8C /* FTS5CustomTokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AD341DABAF4A0056AF8C /* FTS5CustomTokenizer.swift */; };
5698AD381DABAF4A0056AF8C /* FTS5CustomTokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AD341DABAF4A0056AF8C /* FTS5CustomTokenizer.swift */; };
5698AD3B1DABAF4A0056AF8C /* FTS5CustomTokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AD341DABAF4A0056AF8C /* FTS5CustomTokenizer.swift */; };
569A98ED2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569A98EC2039B6F3008D7DBF /* Fixits-3.0.swift */; };
569A98EE2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569A98EC2039B6F3008D7DBF /* Fixits-3.0.swift */; };
569A98EF2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569A98EC2039B6F3008D7DBF /* Fixits-3.0.swift */; };
569C1EB51CF07DDD0042627B /* SchedulingWatchdogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */; };
569D6DDE220EF9E100A058A9 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */; };
569D6DDF220EF9E100A058A9 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */; };
569D6DE0220EF9E100A058A9 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */; };
569EF0E2200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569EF0E1200D2D8400A9FA45 /* DatabaseRegion.swift */; };
569EF0E3200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569EF0E1200D2D8400A9FA45 /* DatabaseRegion.swift */; };
569EF0E4200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569EF0E1200D2D8400A9FA45 /* DatabaseRegion.swift */; };
@ -563,6 +583,11 @@
56A8C2331D1914540096E9D4 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; };
56A8C2471D1918F00096E9D4 /* FoundationNSUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C2361D1914790096E9D4 /* FoundationNSUUIDTests.swift */; };
56A8C2481D1918F00096E9D4 /* FoundationUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C21E1D1914110096E9D4 /* FoundationUUIDTests.swift */; };
56AE64122229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */; };
56AE64132229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */; };
56AE64142229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */; };
56AE6424222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */; };
56AE6425222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */; };
56AF746F1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */; };
56B021C91D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */; };
56B021CD1D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */; };
@ -573,6 +598,8 @@
56B7F42A1BE14A1900E39BBF /* CGFloatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B7F4291BE14A1900E39BBF /* CGFloatTests.swift */; };
56B7F43A1BEB42D500E39BBF /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B7F4391BEB42D500E39BBF /* Migration.swift */; };
56B7F43B1BEB42D500E39BBF /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B7F4391BEB42D500E39BBF /* Migration.swift */; };
56B86E79220FF4E000524C16 /* SQLLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B86E72220FF4E000524C16 /* SQLLiteralTests.swift */; };
56B86E7A220FF4E000524C16 /* SQLLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B86E72220FF4E000524C16 /* SQLLiteralTests.swift */; };
56B9649D1DA51B4C0002DA19 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9649C1DA51B4C0002DA19 /* FTS5.swift */; };
56B964A01DA51B4C0002DA19 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9649C1DA51B4C0002DA19 /* FTS5.swift */; };
56B964A31DA51B4C0002DA19 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9649C1DA51B4C0002DA19 /* FTS5.swift */; };
@ -584,15 +611,6 @@
56B964BF1DA51D0A0002DA19 /* FTS5Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B964B81DA51D0A0002DA19 /* FTS5Pattern.swift */; };
56BB6EA91D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */; };
56BB6EAC1D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */; };
56BF6D2F1DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2C1DEF47DA006039A3 /* Fixits-0-84-0.swift */; };
56BF6D321DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2C1DEF47DA006039A3 /* Fixits-0-84-0.swift */; };
56BF6D351DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2C1DEF47DA006039A3 /* Fixits-0-84-0.swift */; };
56BF6D361DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2D1DEF47DA006039A3 /* Fixits-0-90-1.swift */; };
56BF6D391DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2D1DEF47DA006039A3 /* Fixits-0-90-1.swift */; };
56BF6D3C1DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2D1DEF47DA006039A3 /* Fixits-0-90-1.swift */; };
56BF6D3D1DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2E1DEF47DA006039A3 /* Fixits-Swift2.swift */; };
56BF6D401DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2E1DEF47DA006039A3 /* Fixits-Swift2.swift */; };
56BF6D431DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BF6D2E1DEF47DA006039A3 /* Fixits-Swift2.swift */; };
56C3F7561CF9F12400F6A361 /* DatabaseSavepointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */; };
56CC922C201DFFB900CB597E /* DropWhileCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CC922B201DFFB900CB597E /* DropWhileCursorTests.swift */; };
56CC922D201DFFB900CB597E /* DropWhileCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CC922B201DFFB900CB597E /* DropWhileCursorTests.swift */; };
@ -632,9 +650,6 @@
56CEB5611EAA359A00BFAF62 /* SQLSelectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5441EAA359A00BFAF62 /* SQLSelectable.swift */; };
56CEB5641EAA359A00BFAF62 /* SQLSelectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5441EAA359A00BFAF62 /* SQLSelectable.swift */; };
56CEB5671EAA359A00BFAF62 /* SQLSelectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5441EAA359A00BFAF62 /* SQLSelectable.swift */; };
56D1215A1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D121591ED34978001347D2 /* Fixits-0.109.0.swift */; };
56D1215D1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D121591ED34978001347D2 /* Fixits-0.109.0.swift */; };
56D121601ED34978001347D2 /* Fixits-0.109.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D121591ED34978001347D2 /* Fixits-0.109.0.swift */; };
56D3BE711F4EB1A00034C6D2 /* FetchRecordStructTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D3BE701F4EB1900034C6D2 /* FetchRecordStructTests.swift */; };
56D3BE721F4EB1A00034C6D2 /* FetchRecordStructTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D3BE701F4EB1900034C6D2 /* FetchRecordStructTests.swift */; };
56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */; };
@ -742,8 +757,6 @@
56DE7B281C41302500861EB8 /* FetchNamedValuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DE7B271C41302500861EB8 /* FetchNamedValuesTests.swift */; };
56DE7B2A1C4130AF00861EB8 /* FetchPositionalValuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DE7B291C4130AF00861EB8 /* FetchPositionalValuesTests.swift */; };
56DE7B2C1C41311900861EB8 /* FetchRecordClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DE7B2B1C41311900861EB8 /* FetchRecordClassTests.swift */; };
56E06F061E859064008AE2A4 /* Fixits-0.102.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E06F051E859064008AE2A4 /* Fixits-0.102.0.swift */; };
56E06F181E85906E008AE2A4 /* Fixits-0.102.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E06F051E859064008AE2A4 /* Fixits-0.102.0.swift */; };
56E5D7D41B4D3FEE00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56E5D7CA1B4D3FED00430942 /* GRDB.framework */; };
56E5D7FE1B4D422E00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC3773F319C8CBB3004FCF85 /* GRDB.framework */; };
56E5D8041B4D424400430942 /* GRDBTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */; };
@ -752,6 +765,12 @@
56E5D82D1B4D438800430942 /* GRDB-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2393C61ABE35F8003FF113 /* GRDB-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; };
56E8CE0E1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */; };
56E8CE111BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */; };
56E9FACB221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FACA221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift */; };
56E9FACC221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FACA221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift */; };
56E9FACD221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FACA221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift */; };
56E9FAD8221053DD00C703A8 /* SQLLiteral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAD7221053DC00C703A8 /* SQLLiteral.swift */; };
56E9FAD9221053DD00C703A8 /* SQLLiteral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAD7221053DC00C703A8 /* SQLLiteral.swift */; };
56E9FADA221053DD00C703A8 /* SQLLiteral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAD7221053DC00C703A8 /* SQLLiteral.swift */; };
56EA63C5209C7CE3009715B8 /* DerivableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA63C4209C7CE3009715B8 /* DerivableRequestTests.swift */; };
56EA63C6209C7CE3009715B8 /* DerivableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA63C4209C7CE3009715B8 /* DerivableRequestTests.swift */; };
56EA86951C91DFE7002BB4DF /* DatabaseReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA86931C91DFE7002BB4DF /* DatabaseReaderTests.swift */; };
@ -761,13 +780,13 @@
56F26C1C1CEE3F32007969C4 /* RowAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F03C11CE5D3AA00DE108F /* RowAdapterTests.swift */; };
56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */; };
56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */; };
56F3E7631E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; };
56F3E7661E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; };
56F3E7691E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; };
56F5ABD91D814330001F60CB /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; };
56F5ABDA1D814330001F60CB /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; };
56F5ABDC1D814330001F60CB /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; };
56F5ABDD1D814330001F60CB /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; };
56FBFED92210731A00945324 /* SQLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FBFED82210731A00945324 /* SQLRequest.swift */; };
56FBFEDA2210731A00945324 /* SQLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FBFED82210731A00945324 /* SQLRequest.swift */; };
56FBFEDB2210731A00945324 /* SQLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FBFED82210731A00945324 /* SQLRequest.swift */; };
56FC98781D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FC98771D969DEF00E3C842 /* SQLExpression+QueryInterface.swift */; };
56FC987B1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FC98771D969DEF00E3C842 /* SQLExpression+QueryInterface.swift */; };
56FC987E1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FC98771D969DEF00E3C842 /* SQLExpression+QueryInterface.swift */; };
@ -786,13 +805,6 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
560C97D11C0E22D300BF8471 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56CA21FE1BB414FE009A04C5 /* SQLite.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = EE247B3B1C3F3ED000AE3E12;
remoteInfo = "SQLite Mac";
};
560C97D31C0E22D300BF8471 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DC3773EA19C8CBB3004FCF85 /* Project object */;
@ -932,8 +944,13 @@
5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+DatabaseValueConvertible.swift"; sourceTree = "<group>"; };
5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Count.swift"; sourceTree = "<group>"; };
5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationCombineTests.swift; sourceTree = "<group>"; };
5615B25A222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughSQLDerivationTests.swift; sourceTree = "<group>"; };
5615B269222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughRowScopeTests.swift; sourceTree = "<group>"; };
5615B274222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughFetchableRecordTests.swift; sourceTree = "<group>"; };
5615B287222B17BF00061C1C /* AssociationHasOneThroughDecodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughDecodableRecordTests.swift; sourceTree = "<group>"; };
561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDecimalNumberTests.swift; sourceTree = "<group>"; };
5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProtocols.swift; sourceTree = "<group>"; };
56172947223533F40006E219 /* EncodableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodableRecord.swift; sourceTree = "<group>"; };
562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = "<group>"; };
5623932F1DEDFC5700A6B01F /* AnyCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCursorTests.swift; sourceTree = "<group>"; };
5623934D1DEDFEFB00A6B01F /* EnumeratedCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedCursorTests.swift; sourceTree = "<group>"; };
@ -1064,7 +1081,7 @@
5674A6E21F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseValueConvertible+Encodable.swift"; sourceTree = "<group>"; };
5674A6E31F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseValueConvertible+Decodable.swift"; sourceTree = "<group>"; };
5674A6F21F307F600095F066 /* FetchableRecord+Decodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+Decodable.swift"; sourceTree = "<group>"; };
5674A6F31F307F600095F066 /* PersistableRecord+Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Encodable.swift"; sourceTree = "<group>"; };
5674A6F31F307F600095F066 /* EncodableRecord+Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EncodableRecord+Encodable.swift"; sourceTree = "<group>"; };
5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseValueConvertible+ReferenceConvertible.swift"; sourceTree = "<group>"; };
5674A70A1F3087700095F066 /* DatabaseValueConvertibleDecodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleDecodableTests.swift; sourceTree = "<group>"; };
5674A70B1F3087700095F066 /* DatabaseValueConvertibleEncodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleEncodableTests.swift; sourceTree = "<group>"; };
@ -1073,10 +1090,11 @@
567A80521D41350C00C7DCEC /* IndexInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndexInfoTests.swift; sourceTree = "<group>"; };
567DAF141EAB61ED00FC0928 /* grdb_config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = grdb_config.h; sourceTree = "<group>"; };
567DAF341EAB789800FC0928 /* DatabaseLogErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseLogErrorTests.swift; sourceTree = "<group>"; };
567ECE4E2222E431009245CA /* GRDB-4.0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GRDB-4.0.swift"; sourceTree = "<group>"; };
567F0B2C220F0E2E00D111FB /* SQLInterpolationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLInterpolationTests.swift; sourceTree = "<group>"; };
567F45A71F888B2600030B59 /* TruncateOptimizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncateOptimizationTests.swift; sourceTree = "<group>"; };
568068301EBBA26100EFB8AA /* SQLRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLRequestTests.swift; sourceTree = "<group>"; };
5687359E1CEDE16C009B9116 /* Betty.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Betty.jpeg; sourceTree = "<group>"; };
56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-1.2.swift"; sourceTree = "<group>"; };
568D13182207213E00674B58 /* SQLSelectQueryGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLSelectQueryGenerator.swift; sourceTree = "<group>"; };
5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsertRecordCodableTests.swift; sourceTree = "<group>"; };
5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRecordCodableTests.swift; sourceTree = "<group>"; };
@ -1096,6 +1114,9 @@
569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolSchemaCacheTests.swift; sourceTree = "<group>"; };
569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolCollationTests.swift; sourceTree = "<group>"; };
569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolFunctionTests.swift; sourceTree = "<group>"; };
56959615222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyThroughSQLTests.swift; sourceTree = "<group>"; };
56959628222C462D002CB7C9 /* HasManyThroughAssociation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HasManyThroughAssociation.swift; sourceTree = "<group>"; };
56959632222D056D002CB7C9 /* AssociationHasManySQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManySQLTests.swift; sourceTree = "<group>"; };
5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryInterfaceExtensibilityTests.swift; sourceTree = "<group>"; };
5698AC361D9E5A590056AF8C /* FTS3Pattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3Pattern.swift; sourceTree = "<group>"; };
5698AC3F1DA2BED90056AF8C /* FTS3PatternTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3PatternTests.swift; sourceTree = "<group>"; };
@ -1113,8 +1134,8 @@
5698AD151DAAD16F0056AF8C /* FTS5Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tokenizer.swift; sourceTree = "<group>"; };
5698AD201DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizer.swift; sourceTree = "<group>"; };
5698AD341DABAF4A0056AF8C /* FTS5CustomTokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5CustomTokenizer.swift; sourceTree = "<group>"; };
569A98EC2039B6F3008D7DBF /* Fixits-3.0.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-3.0.swift"; sourceTree = "<group>"; };
569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchedulingWatchdogTests.swift; sourceTree = "<group>"; };
569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLInterpolation.swift; sourceTree = "<group>"; };
569EF0E1200D2D8400A9FA45 /* DatabaseRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRegion.swift; sourceTree = "<group>"; };
56A238131B9C74A90082EB20 /* DatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = "<group>"; };
56A238141B9C74A90082EB20 /* DatabaseQueueInMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryTests.swift; sourceTree = "<group>"; };
@ -1155,6 +1176,8 @@
56A8C21E1D1914110096E9D4 /* FoundationUUIDTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationUUIDTests.swift; sourceTree = "<group>"; };
56A8C22F1D1914540096E9D4 /* UUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = "<group>"; };
56A8C2361D1914790096E9D4 /* FoundationNSUUIDTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSUUIDTests.swift; sourceTree = "<group>"; };
56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasOneThroughAssociation.swift; sourceTree = "<group>"; };
56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughSQLTests.swift; sourceTree = "<group>"; };
56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleEscapingTests.swift; sourceTree = "<group>"; };
56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePersistableRecordPersistenceConflictPolicyTests.swift; sourceTree = "<group>"; };
56B14E7E1D4DAE54000BF4A3 /* RowFromDictionaryLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFromDictionaryLiteralTests.swift; sourceTree = "<group>"; };
@ -1162,8 +1185,9 @@
56B6EF55208CB4E3002F0ACB /* ColumnExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressionTests.swift; sourceTree = "<group>"; };
56B7F4291BE14A1900E39BBF /* CGFloatTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGFloatTests.swift; sourceTree = "<group>"; };
56B7F4391BEB42D500E39BBF /* Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = "<group>"; };
56B8C2401CA1758F00510325 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = "Realm/build/osx/swift-4.2.1/Realm.framework"; sourceTree = "<group>"; };
56B8C2411CA1758F00510325 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = "Realm/build/osx/swift-4.2.1/RealmSwift.framework"; sourceTree = "<group>"; };
56B86E72220FF4E000524C16 /* SQLLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLLiteralTests.swift; sourceTree = "<group>"; };
56B8C2401CA1758F00510325 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = "Realm/build/osx/swift-5.0/Realm.framework"; sourceTree = "<group>"; };
56B8C2411CA1758F00510325 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = "Realm/build/osx/swift-5.0/RealmSwift.framework"; sourceTree = "<group>"; };
56B8F49A1B4E2F3600C24296 /* GRDB.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GRDB.xcconfig; sourceTree = "<group>"; };
56B9649C1DA51B4C0002DA19 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = "<group>"; };
56B964B01DA51D010002DA19 /* FTS5TokenizerDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5TokenizerDescriptor.swift; sourceTree = "<group>"; };
@ -1186,9 +1210,6 @@
56BB86211BA988F2001F9168 /* FMResultSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMResultSet.h; sourceTree = "<group>"; };
56BB86221BA988F2001F9168 /* FMResultSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMResultSet.m; sourceTree = "<group>"; };
56BB862D1BA98933001F9168 /* GRDBPerformanceComparisonTests-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GRDBPerformanceComparisonTests-Bridging.h"; sourceTree = "<group>"; };
56BF6D2C1DEF47DA006039A3 /* Fixits-0-84-0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0-84-0.swift"; sourceTree = "<group>"; };
56BF6D2D1DEF47DA006039A3 /* Fixits-0-90-1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0-90-1.swift"; sourceTree = "<group>"; };
56BF6D2E1DEF47DA006039A3 /* Fixits-Swift2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-Swift2.swift"; sourceTree = "<group>"; };
56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSavepointTests.swift; sourceTree = "<group>"; };
56C48E731C9A9923005DF1D9 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
56C494401ED7255500CC72AF /* GRDBDeploymentTarget.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GRDBDeploymentTarget.xcconfig; sourceTree = "<group>"; };
@ -1208,7 +1229,6 @@
56CEB5421EAA359A00BFAF62 /* SQLExpression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpression.swift; sourceTree = "<group>"; };
56CEB5431EAA359A00BFAF62 /* SQLOrdering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLOrdering.swift; sourceTree = "<group>"; };
56CEB5441EAA359A00BFAF62 /* SQLSelectable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLSelectable.swift; sourceTree = "<group>"; };
56D121591ED34978001347D2 /* Fixits-0.109.0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.109.0.swift"; sourceTree = "<group>"; };
56D3BE701F4EB1900034C6D2 /* FetchRecordStructTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRecordStructTests.swift; sourceTree = "<group>"; };
56D5075D1F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryKeyInfoTests.swift; sourceTree = "<group>"; };
56D507821F6D7A4500AE1C5B /* InsertRecordStructTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertRecordStructTests.swift; sourceTree = "<group>"; };
@ -1227,12 +1247,13 @@
56DE7B2E1C42B23B00861EB8 /* PerformanceModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PerformanceModel.xcdatamodel; sourceTree = "<group>"; };
56DE7B341C42B37E00861EB8 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
56DE7B361C42BBBB00861EB8 /* PerformanceCoreDataTests.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = PerformanceCoreDataTests.sqlite; sourceTree = "<group>"; };
56E06F051E859064008AE2A4 /* Fixits-0.102.0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.102.0.swift"; sourceTree = "<group>"; };
56E5D7CA1B4D3FED00430942 /* GRDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GRDB.framework; sourceTree = BUILT_PRODUCTS_DIR; };
56E5D7D31B4D3FEE00430942 /* GRDBiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56E5D7F91B4D422D00430942 /* GRDBOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleFetchTests.swift; sourceTree = "<group>"; };
56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleFetchTests.swift; sourceTree = "<group>"; };
56E9FACA221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+QueryInterface.swift"; sourceTree = "<group>"; };
56E9FAD7221053DC00C703A8 /* SQLLiteral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLLiteral.swift; sourceTree = "<group>"; };
56EA63C4209C7CE3009715B8 /* DerivableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivableRequestTests.swift; sourceTree = "<group>"; };
56EA86931C91DFE7002BB4DF /* DatabaseReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseReaderTests.swift; sourceTree = "<group>"; };
56EA869D1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolReadOnlyTests.swift; sourceTree = "<group>"; };
@ -1240,7 +1261,7 @@
56ED8A7E1DAB8D6800BD0ABC /* FTS5WrapperTokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizerTests.swift; sourceTree = "<group>"; };
56F0B98E1B6001C600A2F135 /* FoundationNSDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDateTests.swift; sourceTree = "<group>"; };
56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = "<group>"; };
56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.101.1.swift"; sourceTree = "<group>"; };
56FBFED82210731A00945324 /* SQLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLRequest.swift; sourceTree = "<group>"; };
56FC98771D969DEF00E3C842 /* SQLExpression+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SQLExpression+QueryInterface.swift"; sourceTree = "<group>"; };
56FDECE11BB32DFD009AD709 /* RowFromStatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFromStatementTests.swift; sourceTree = "<group>"; };
56FEE7FA1F47253700D930EA /* TableRecordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableRecordTests.swift; sourceTree = "<group>"; };
@ -1536,6 +1557,7 @@
56CEB5421EAA359A00BFAF62 /* SQLExpression.swift */,
56FC98771D969DEF00E3C842 /* SQLExpression+QueryInterface.swift */,
5653EC0B2098738B00F46237 /* SQLGenerationContext.swift */,
56E9FACA221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift */,
56CEB5431EAA359A00BFAF62 /* SQLOrdering.swift */,
56D91AA12205E03700770D8D /* SQLRelation.swift */,
56CEB5441EAA359A00BFAF62 /* SQLSelectable.swift */,
@ -1593,8 +1615,15 @@
5653EAC920944B4D00F46237 /* AssociationBelongsToSQLTests.swift */,
5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */,
5653EACF20944B4E00F46237 /* AssociationChainSQLTests.swift */,
56959632222D056D002CB7C9 /* AssociationHasManySQLTests.swift */,
56959615222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift */,
5653EACA20944B4D00F46237 /* AssociationHasOneSQLDerivationTests.swift */,
5653EACC20944B4D00F46237 /* AssociationHasOneSQLTests.swift */,
5615B287222B17BF00061C1C /* AssociationHasOneThroughDecodableRecordTests.swift */,
5615B274222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift */,
5615B269222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift */,
5615B25A222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift */,
56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */,
5653EAD220944B4E00F46237 /* AssociationParallelDecodableRecordTests.swift */,
5653EACD20944B4D00F46237 /* AssociationParallelRowScopesTests.swift */,
5653EAD520944B4F00F46237 /* AssociationParallelSQLTests.swift */,
@ -1613,7 +1642,9 @@
5653EAFF20944C7C00F46237 /* ForeignKey.swift */,
5653EAF920944C7B00F46237 /* ForeignKeyRequest.swift */,
5653EAFB20944C7B00F46237 /* HasManyAssociation.swift */,
56959628222C462D002CB7C9 /* HasManyThroughAssociation.swift */,
5653EB0220944C7C00F46237 /* HasOneAssociation.swift */,
56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */,
);
path = Association;
sourceTree = "<group>";
@ -1748,6 +1779,8 @@
56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */,
56A2381D1B9C74A90082EB20 /* Row */,
569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */,
567F0B2C220F0E2E00D111FB /* SQLInterpolationTests.swift */,
56B86E72220FF4E000524C16 /* SQLLiteralTests.swift */,
568068301EBBA26100EFB8AA /* SQLRequestTests.swift */,
56A238201B9C74A90082EB20 /* Statement */,
56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */,
@ -1841,6 +1874,9 @@
567404871CEF84C8003ED5CC /* RowAdapter.swift */,
56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */,
560A37A61C8FF6E500949E71 /* SerializedDatabase.swift */,
569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */,
56FBFED82210731A00945324 /* SQLRequest.swift */,
56E9FAD7221053DC00C703A8 /* SQLLiteral.swift */,
56A238781B9C75030082EB20 /* Statement.swift */,
566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */,
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */,
@ -1862,12 +1898,13 @@
56A2389F1B9C753B0082EB20 /* Record */ = {
isa = PBXGroup;
children = (
56172947223533F40006E219 /* EncodableRecord.swift */,
5674A6F31F307F600095F066 /* EncodableRecord+Encodable.swift */,
56CEB4F01EAA2EFA00BFAF62 /* FetchableRecord.swift */,
5674A6F21F307F600095F066 /* FetchableRecord+Decodable.swift */,
56D51CFF1EA789FA0074638A /* FetchableRecord+TableRecord.swift */,
31A7787C1C6A4DD600F507F6 /* FetchedRecordsController.swift */,
560D92441C672C4B00F4F92B /* PersistableRecord.swift */,
5674A6F31F307F600095F066 /* PersistableRecord+Encodable.swift */,
56A238A11B9C753B0082EB20 /* Record.swift */,
560D92461C672C4B00F4F92B /* TableRecord.swift */,
);
@ -1923,21 +1960,6 @@
path = fmdb/src/fmdb;
sourceTree = "<group>";
};
56BF6D2B1DEF47DA006039A3 /* Legacy */ = {
isa = PBXGroup;
children = (
56BF6D2E1DEF47DA006039A3 /* Fixits-Swift2.swift */,
56BF6D2C1DEF47DA006039A3 /* Fixits-0-84-0.swift */,
56BF6D2D1DEF47DA006039A3 /* Fixits-0-90-1.swift */,
56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */,
56E06F051E859064008AE2A4 /* Fixits-0.102.0.swift */,
56D121591ED34978001347D2 /* Fixits-0.109.0.swift */,
56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */,
569A98EC2039B6F3008D7DBF /* Fixits-3.0.swift */,
);
path = Legacy;
sourceTree = "<group>";
};
56CA21FF1BB414FE009A04C5 /* Products */ = {
isa = PBXGroup;
children = (
@ -1952,6 +1974,14 @@
name = Products;
sourceTree = "<group>";
};
56D7E449221595FE0052464B /* Fixit */ = {
isa = PBXGroup;
children = (
567ECE4E2222E431009245CA /* GRDB-4.0.swift */,
);
path = Fixit;
sourceTree = "<group>";
};
56DAA2C41DE99D8D006E10C8 /* Cursor */ = {
isa = PBXGroup;
children = (
@ -2042,8 +2072,8 @@
isa = PBXGroup;
children = (
56A2386F1B9C75030082EB20 /* Core */,
56D7E449221595FE0052464B /* Fixit */,
5698AC291D9E5A480056AF8C /* FTS */,
56BF6D2B1DEF47DA006039A3 /* Legacy */,
56A238911B9C750B0082EB20 /* Migration */,
56300B6D1C53F592005A543B /* QueryInterface */,
56A2389F1B9C753B0082EB20 /* Record */,
@ -2116,7 +2146,6 @@
buildRules = (
);
dependencies = (
560C97D01C0E22D300BF8471 /* PBXTargetDependency */,
560C97D21C0E22D300BF8471 /* PBXTargetDependency */,
);
name = GRDBOSXPerformanceTests;
@ -2480,7 +2509,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
569A98EF2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */,
5616AAF3207CD45E00AC3664 /* RequestProtocols.swift in Sources */,
56B964BF1DA51D0A0002DA19 /* FTS5Pattern.swift in Sources */,
56D51D061EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */,
@ -2502,10 +2530,11 @@
5653EB0B20944C7C00F46237 /* Association.swift in Sources */,
564CE59F21B7A8B500652B19 /* ValueObservation+DistinctUntilChanged.swift in Sources */,
56F5ABDA1D814330001F60CB /* NSData.swift in Sources */,
56D121601ED34978001347D2 /* Fixits-0.109.0.swift in Sources */,
564F9C341F07611900877A00 /* DatabaseFunction.swift in Sources */,
566475A81D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */,
5659F4961EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
56172950223533F40006E219 /* EncodableRecord.swift in Sources */,
56AE64142229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */,
5613ED3721A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */,
5659F48E1EA8D94E004A4992 /* Utils.swift in Sources */,
564CE43321AA901800652B19 /* ValueObserver.swift in Sources */,
@ -2527,25 +2556,26 @@
5613ED4A21A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */,
565490D51D5AE252005622CB /* DatabaseValueConvertible+RawRepresentable.swift in Sources */,
565490C51D5AE236005622CB /* SerializedDatabase.swift in Sources */,
56F3E7691E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */,
5698AC7E1DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */,
564CE52221B3129A00652B19 /* ValueObservation+MapReducer.swift in Sources */,
5653EB1A20944C7C00F46237 /* ForeignKey.swift in Sources */,
5698AC3D1D9E5A590056AF8C /* FTS3Pattern.swift in Sources */,
563EF45D2163309F007DAACD /* Inflections.swift in Sources */,
569D6DE0220EF9E100A058A9 /* SQLInterpolation.swift in Sources */,
567ECE512222E431009245CA /* GRDB-4.0.swift in Sources */,
565490BF1D5AE236005622CB /* DatabaseValueConvertible.swift in Sources */,
566B91191FA4C3F50012D5B0 /* DatabaseCollation.swift in Sources */,
5613ED4E21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */,
5613ED5621A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */,
56CEB5591EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
5674A6FF1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */,
56E06F181E85906E008AE2A4 /* Fixits-0.102.0.swift in Sources */,
56E9FAD9221053DD00C703A8 /* SQLLiteral.swift in Sources */,
56B964A31DA51B4C0002DA19 /* FTS5.swift in Sources */,
56D91AAB2205F2F100770D8D /* DatabasePromise.swift in Sources */,
566475A01D97D8A000FF74B8 /* SQLCollection.swift in Sources */,
565490BB1D5AE236005622CB /* DatabaseReader.swift in Sources */,
5659F4A61EA8D997004A4992 /* Result.swift in Sources */,
5674A6F81F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */,
5674A6F81F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */,
565490C31D5AE236005622CB /* RowAdapter.swift in Sources */,
5698AD3B1DABAF4A0056AF8C /* FTS5CustomTokenizer.swift in Sources */,
5659F49E1EA8D989004A4992 /* Pool.swift in Sources */,
@ -2556,15 +2586,13 @@
565490DF1D5AE252005622CB /* TableDefinition.swift in Sources */,
565490B71D5AE236005622CB /* Database.swift in Sources */,
5674A6E81F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */,
56873BF21F2CB400004D24B4 /* Fixits-1.2.swift in Sources */,
56BF6D431DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */,
56E9FACD221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */,
5653EB2320944C7C00F46237 /* HasOneAssociation.swift in Sources */,
565490C01D5AE236005622CB /* DatabaseWriter.swift in Sources */,
565490C41D5AE236005622CB /* SchedulingWatchdog.swift in Sources */,
566B91311FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */,
56CEB54B1EAA359A00BFAF62 /* Column.swift in Sources */,
565490CC1D5AE252005622CB /* (null) in Sources */,
56BF6D351DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */,
566B91291FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
5671FC261DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */,
5653EC142098738B00F46237 /* SQLGenerationContext.swift in Sources */,
@ -2575,6 +2603,7 @@
565490C61D5AE236005622CB /* Statement.swift in Sources */,
56CEB5071EAA2F4D00BFAF62 /* FTS4.swift in Sources */,
56CEB4F71EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
56FBFEDB2210731A00945324 /* SQLRequest.swift in Sources */,
56F5ABD91D814330001F60CB /* Data.swift in Sources */,
565490D41D5AE252005622CB /* (null) in Sources */,
566475D21D981D5E00FF74B8 /* SQLFunctions.swift in Sources */,
@ -2586,6 +2615,7 @@
565490C81D5AE252005622CB /* CGFloat.swift in Sources */,
56F5ABDC1D814330001F60CB /* URL.swift in Sources */,
569EF0E4200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */,
5695962B222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */,
565490D31D5AE252005622CB /* (null) in Sources */,
566475D91D981D5E00FF74B8 /* SQLOperators.swift in Sources */,
566B910F1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
@ -2595,7 +2625,6 @@
565490D61D5AE252005622CB /* StandardLibrary.swift in Sources */,
565490B81D5AE236005622CB /* DatabaseError.swift in Sources */,
564CE4D821B2DEB600652B19 /* ValueObservation+CompactMap.swift in Sources */,
56BF6D3C1DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */,
56DAA2E11DE9C827006E10C8 /* Cursor.swift in Sources */,
5653EB0520944C7C00F46237 /* BelongsToAssociation.swift in Sources */,
5644DE6F20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */,
@ -2628,13 +2657,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56BF6D401DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */,
5636E9BF1D22574100B9B05F /* FetchRequest.swift in Sources */,
563EF42E2161180D007DAACD /* AssociationAggregate.swift in Sources */,
56BB6EAC1D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */,
56300B791C53F592005A543B /* QueryInterfaceRequest.swift in Sources */,
56CEB5561EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
5674A6F91F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */,
5674A6F91F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */,
5605F18E1C6B1A8700235C62 /* SQLCollatedExpression.swift in Sources */,
566475D61D981D5E00FF74B8 /* SQLOperators.swift in Sources */,
5698AD381DABAF4A0056AF8C /* FTS5CustomTokenizer.swift in Sources */,
@ -2653,7 +2681,6 @@
566475CF1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */,
5659F49B1EA8D989004A4992 /* Pool.swift in Sources */,
5653EB2220944C7C00F46237 /* HasOneAssociation.swift in Sources */,
569A98EE2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */,
56CEB51C1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */,
5698AC3A1D9E5A590056AF8C /* FTS3Pattern.swift in Sources */,
5664759D1D97D8A000FF74B8 /* SQLCollection.swift in Sources */,
@ -2662,7 +2689,6 @@
563EF416215F87EB007DAACD /* OrderedDictionary.swift in Sources */,
563363C51C942C37000BE133 /* DatabaseWriter.swift in Sources */,
5698AD1B1DAAD17D0056AF8C /* FTS5Tokenizer.swift in Sources */,
56F3E7661E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */,
5653EB0420944C7C00F46237 /* BelongsToAssociation.swift in Sources */,
56B964B41DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
560A37A81C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
@ -2672,13 +2698,13 @@
5605F16A1C672E4000235C62 /* NSString.swift in Sources */,
566B910C1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
56A8C2331D1914540096E9D4 /* UUID.swift in Sources */,
56BF6D321DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */,
560D92411C672C3E00F4F92B /* DatabaseValueConvertible.swift in Sources */,
566475BD1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */,
56A238A51B9C753B0082EB20 /* Record.swift in Sources */,
5671FC231DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */,
56D51D031EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */,
560D924C1C672C4B00F4F92B /* TableRecord.swift in Sources */,
5617294F223533F40006E219 /* EncodableRecord.swift in Sources */,
564CE52121B3129A00652B19 /* ValueObservation+MapReducer.swift in Sources */,
5653EC132098738B00F46237 /* SQLGenerationContext.swift in Sources */,
5657AB121D10899D006283EF /* URL.swift in Sources */,
@ -2688,6 +2714,7 @@
56DAA2DE1DE9C827006E10C8 /* Cursor.swift in Sources */,
560A37A51C8F625000949E71 /* DatabasePool.swift in Sources */,
5674A7081F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift in Sources */,
56AE64132229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */,
56A2388C1B9C75030082EB20 /* Statement.swift in Sources */,
5659F4931EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
56D91AB12205F8AC00770D8D /* SQLSelectQuery.swift in Sources */,
@ -2704,7 +2731,7 @@
56CEB4F41EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
569531201C907A8C00CF1A2B /* DatabaseSchemaCache.swift in Sources */,
5605F15E1C672E4000235C62 /* DatabaseDateComponents.swift in Sources */,
56BF6D391DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */,
5695962A222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */,
563363C11C942C04000BE133 /* DatabaseReader.swift in Sources */,
5653EB1920944C7C00F46237 /* ForeignKey.swift in Sources */,
5659F4A31EA8D997004A4992 /* Result.swift in Sources */,
@ -2715,11 +2742,13 @@
563B06AC217EF0CC00B38F35 /* ValueObservation.swift in Sources */,
56D91AAA2205F2F100770D8D /* DatabasePromise.swift in Sources */,
5698AC7B1DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */,
569D6DDF220EF9E100A058A9 /* SQLInterpolation.swift in Sources */,
56A238821B9C75030082EB20 /* DatabaseError.swift in Sources */,
56D1215D1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */,
5653EB0D20944C7C00F46237 /* HasManyAssociation.swift in Sources */,
567ECE502222E431009245CA /* GRDB-4.0.swift in Sources */,
566AD8B51D5318F4002EC1A8 /* TableDefinition.swift in Sources */,
5698AD241DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */,
56FBFED92210731A00945324 /* SQLRequest.swift in Sources */,
5644DE6E20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */,
C96C0F2C2084A459006B2981 /* SQLiteDateParser.swift in Sources */,
5605F1681C672E4000235C62 /* NSNumber.swift in Sources */,
@ -2727,12 +2756,14 @@
564CE5AD21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */,
56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */,
5605F1741C672E4000235C62 /* StandardLibrary.swift in Sources */,
56E9FACC221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */,
560D92481C672C4B00F4F92B /* PersistableRecord.swift in Sources */,
5613ED4521A95B2C00DC7A68 /* ValueReducer.swift in Sources */,
5613ED5121A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
560D92431C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */,
5613ED3621A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */,
566475A51D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */,
56E9FAD8221053DD00C703A8 /* SQLLiteral.swift in Sources */,
5653EB0720944C7C00F46237 /* ForeignKeyRequest.swift in Sources */,
5657AABC1D107001006283EF /* NSData.swift in Sources */,
31A778841C6A4E0600F507F6 /* FetchedRecordsController.swift in Sources */,
@ -2742,7 +2773,6 @@
566B91261FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
566B912E1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */,
56CEB5641EAA359A00BFAF62 /* SQLSelectable.swift in Sources */,
56873BEF1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */,
56D91AA32205E03700770D8D /* SQLRelation.swift in Sources */,
564F9C311F07611600877A00 /* DatabaseFunction.swift in Sources */,
564CE59E21B7A8B500652B19 /* ValueObservation+DistinctUntilChanged.swift in Sources */,
@ -2788,6 +2818,7 @@
56EA63C6209C7CE3009715B8 /* DerivableRequestTests.swift in Sources */,
5674A71A1F3087710095F066 /* DatabaseValueConvertibleDecodableTests.swift in Sources */,
5698AC071D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */,
567F0B2E220F0E2E00D111FB /* SQLInterpolationTests.swift in Sources */,
56C3F7561CF9F12400F6A361 /* DatabaseSavepointTests.swift in Sources */,
56B021CD1D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */,
563B0706218627F800B38F35 /* ValueObservationRowTests.swift in Sources */,
@ -2805,6 +2836,7 @@
5698AC4D1DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */,
56B15D0B1CD4C35100A24C8B /* FetchedRecordsControllerTests.swift in Sources */,
56A238681B9C74A90082EB20 /* RecordInitializersTests.swift in Sources */,
5615B261222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */,
56CC9252201E093F00CB597E /* PrefixCursorTests.swift in Sources */,
563363B01C933FF8000BE133 /* MutablePersistableRecordTests.swift in Sources */,
5665FA152129C9D6004D8612 /* DatabaseDateDecodingStrategyTests.swift in Sources */,
@ -2815,12 +2847,14 @@
56176C6B1EACCCC9000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
56300B621C53C42C005A543B /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */,
56EA869F1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift in Sources */,
5615B288222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */,
567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
5657AB621D108BA9006283EF /* FoundationNSURLTests.swift in Sources */,
5657AB6A1D108BA9006283EF /* FoundationURLTests.swift in Sources */,
563B06C42185D29F00B38F35 /* ValueObservationExtentTests.swift in Sources */,
564FCE5F20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
5653EADF20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
5695961D222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */,
56B6EF57208CB4E3002F0ACB /* ColumnExpressionTests.swift in Sources */,
@ -2829,8 +2863,10 @@
56741EAC1E66A8B3003E422D /* FetchRequestTests.swift in Sources */,
5672DE5C1CDB72520022BA81 /* DatabaseQueueBackupTests.swift in Sources */,
56A2385E1B9C74A90082EB20 /* RecordCopyTests.swift in Sources */,
56B86E7A220FF4E000524C16 /* SQLLiteralTests.swift in Sources */,
56176C6C1EACCCC9000F3F2B /* FTS5PatternTests.swift in Sources */,
563363B21C933FF8000BE133 /* PersistableRecordTests.swift in Sources */,
5615B26B222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
56A238421B9C74A90082EB20 /* DatabaseValueConversionTests.swift in Sources */,
564E73E0203D50B9000C443C /* JoinSupportTests.swift in Sources */,
5674A7121F3087710095F066 /* DatabaseValueConvertibleEncodableTests.swift in Sources */,
@ -2859,6 +2895,7 @@
56A8C2471D1918F00096E9D4 /* FoundationNSUUIDTests.swift in Sources */,
5698AC441DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */,
5698ACBA1DA6285E0056AF8C /* FTS3TokenizerTests.swift in Sources */,
5615B276222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */,
56176C6F1EACCCC9000F3F2B /* FTS5TokenizerTests.swift in Sources */,
565B0FF01BBC7D980098DE03 /* FetchableRecordTests.swift in Sources */,
5698AC9A1DA4B0430056AF8C /* FTS4RecordTests.swift in Sources */,
@ -2912,7 +2949,9 @@
56A238381B9C74A90082EB20 /* DatabaseTests.swift in Sources */,
5653EAED20944B4F00F46237 /* AssociationBelongsToSQLDerivationTests.swift in Sources */,
564CE4EA21B2E06F00652B19 /* ValueObservationMapTests.swift in Sources */,
56959634222D056D002CB7C9 /* AssociationHasManySQLTests.swift in Sources */,
56300B5F1C53C38F005A543B /* QueryInterfaceRequestTests.swift in Sources */,
56AE6425222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */,
5634B10A1CF9B970005360B9 /* TransactionObserverSavepointsTests.swift in Sources */,
560C97C81BFD0B8400BF8471 /* DatabaseFunctionTests.swift in Sources */,
5653EAD920944B4F00F46237 /* AssociationBelongsToRowScopeTests.swift in Sources */,
@ -2968,6 +3007,7 @@
562756431E963AAC0035B653 /* DatabaseWriterTests.swift in Sources */,
56EA63C5209C7CE3009715B8 /* DerivableRequestTests.swift in Sources */,
56D496BF1D8135D4008276D7 /* TableDefinitionTests.swift in Sources */,
567F0B2D220F0E2E00D111FB /* SQLInterpolationTests.swift in Sources */,
5674A7171F3087710095F066 /* DatabaseValueConvertibleDecodableTests.swift in Sources */,
56D496801D813131008276D7 /* StatementColumnConvertibleFetchTests.swift in Sources */,
563B0705218627F800B38F35 /* ValueObservationRowTests.swift in Sources */,
@ -2985,6 +3025,7 @@
56D496881D81316E008276D7 /* DatabaseValueConversionTests.swift in Sources */,
56D496BA1D813482008276D7 /* DatabaseQueueSchemaCacheTests.swift in Sources */,
56D496781D81309E008276D7 /* RecordSubClassTests.swift in Sources */,
5615B262222AE1B400061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */,
56D4965F1D81304E008276D7 /* FoundationNSURLTests.swift in Sources */,
562393301DEDFC5700A6B01F /* AnyCursorTests.swift in Sources */,
5665FA142129C9D6004D8612 /* DatabaseDateDecodingStrategyTests.swift in Sources */,
@ -2995,12 +3036,14 @@
5653EADC20944B4F00F46237 /* AssociationBelongsToSQLTests.swift in Sources */,
56D5075E1F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift in Sources */,
56176C591EACCCC7000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
5615B289222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */,
56D4968A1D81316E008276D7 /* DatabaseValueConvertibleSubclassTests.swift in Sources */,
56D496601D81304E008276D7 /* FoundationNSUUIDTests.swift in Sources */,
567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
563B06C32185D29F00B38F35 /* ValueObservationExtentTests.swift in Sources */,
56D4968D1D81316E008276D7 /* DatabaseFunctionTests.swift in Sources */,
564FCE5E20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
5695961C222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
5653EADE20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */,
56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
@ -3009,8 +3052,10 @@
5698ACD71DA925420056AF8C /* RowTestCase.swift in Sources */,
56176C7D1EACCD2D000F3F2B /* EncryptionTests.swift in Sources */,
56741EA81E66A8B3003E422D /* FetchRequestTests.swift in Sources */,
56B86E79220FF4E000524C16 /* SQLLiteralTests.swift in Sources */,
56D496831D813147008276D7 /* DatabaseSavepointTests.swift in Sources */,
56D496871D81316E008276D7 /* DatabaseTimestampTests.swift in Sources */,
5615B26A222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
56176C5A1EACCCC7000F3F2B /* FTS5PatternTests.swift in Sources */,
56D496581D81304E008276D7 /* FoundationDateTests.swift in Sources */,
562205F31E420E47005860AC /* DatabaseQueueReleaseMemoryTests.swift in Sources */,
@ -3039,6 +3084,7 @@
562206081E420EA4005860AC /* DatabasePoolReadOnlyTests.swift in Sources */,
5698ACCE1DA8C2620056AF8C /* RecordPrimaryKeyHiddenRowIDTests.swift in Sources */,
56D496751D81309E008276D7 /* RecordEditedTests.swift in Sources */,
5615B275222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */,
56D4966F1D81309E008276D7 /* RecordPrimaryKeyNoneTests.swift in Sources */,
56D496621D81304E008276D7 /* StatementArguments+FoundationTests.swift in Sources */,
56176C5D1EACCCC7000F3F2B /* FTS5TokenizerTests.swift in Sources */,
@ -3092,7 +3138,9 @@
56D496551D812F83008276D7 /* FoundationDataTests.swift in Sources */,
56D4966A1D813086008276D7 /* TableRecord+QueryInterfaceRequestTests.swift in Sources */,
564CE4E921B2E06F00652B19 /* ValueObservationMapTests.swift in Sources */,
56959633222D056D002CB7C9 /* AssociationHasManySQLTests.swift in Sources */,
5653EAEC20944B4F00F46237 /* AssociationBelongsToSQLDerivationTests.swift in Sources */,
56AE6424222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */,
562206071E420EA4005860AC /* DatabasePoolConcurrencyTests.swift in Sources */,
5698ACB61DA6285E0056AF8C /* FTS3TokenizerTests.swift in Sources */,
56D4965C1D81304E008276D7 /* FoundationNSNullTests.swift in Sources */,
@ -3115,11 +3163,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56873BEC1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */,
563EF42D2161180D007DAACD /* AssociationAggregate.swift in Sources */,
56D91AB02205F8AC00770D8D /* SQLSelectQuery.swift in Sources */,
5613ED4C21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */,
56BF6D3D1DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */,
5616AAF1207CD45E00AC3664 /* RequestProtocols.swift in Sources */,
5613ED4421A95B2C00DC7A68 /* ValueReducer.swift in Sources */,
5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */,
@ -3139,7 +3185,6 @@
5605F18D1C6B1A8700235C62 /* SQLCollatedExpression.swift in Sources */,
5613ED5421A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */,
566B91331FA4D3810012D5B0 /* TransactionObserver.swift in Sources */,
56D1215A1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */,
566475D31D981D5E00FF74B8 /* SQLOperators.swift in Sources */,
56CEB5611EAA359A00BFAF62 /* SQLSelectable.swift in Sources */,
56CEB4FA1EAA2F4D00BFAF62 /* FTS3.swift in Sources */,
@ -3159,15 +3204,14 @@
564F9C2D1F075DD200877A00 /* DatabaseFunction.swift in Sources */,
564CE5AC21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */,
5659F4981EA8D989004A4992 /* Pool.swift in Sources */,
5674A6F41F307F600095F066 /* PersistableRecord+Encodable.swift in Sources */,
5674A6F41F307F600095F066 /* EncodableRecord+Encodable.swift in Sources */,
56CEB54C1EAA359A00BFAF62 /* SQLExpressible.swift in Sources */,
5664759A1D97D8A000FF74B8 /* SQLCollection.swift in Sources */,
567404881CEF84C8003ED5CC /* RowAdapter.swift in Sources */,
56E06F061E859064008AE2A4 /* Fixits-0.102.0.swift in Sources */,
5653EB0320944C7C00F46237 /* BelongsToAssociation.swift in Sources */,
563363C41C942C37000BE133 /* DatabaseWriter.swift in Sources */,
5617294E223533F40006E219 /* EncodableRecord.swift in Sources */,
5698AD181DAAD17A0056AF8C /* FTS5Tokenizer.swift in Sources */,
56F3E7631E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */,
56B964B11DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
5653EB252094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */,
@ -3176,8 +3220,8 @@
564CE4D621B2DEB600652B19 /* ValueObservation+CompactMap.swift in Sources */,
56A8C2301D1914540096E9D4 /* UUID.swift in Sources */,
56CEB5011EAA2F4D00BFAF62 /* FTS4.swift in Sources */,
56AE64122229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */,
56CEB55A1EAA359A00BFAF62 /* SQLOrdering.swift in Sources */,
56BF6D2F1DEF47DA006039A3 /* Fixits-0-84-0.swift in Sources */,
56A238811B9C75030082EB20 /* DatabaseError.swift in Sources */,
56D51D001EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */,
566475BA1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */,
@ -3193,6 +3237,7 @@
56DAA2DB1DE9C827006E10C8 /* Cursor.swift in Sources */,
5674A6EB1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */,
56D91AA92205F2F100770D8D /* DatabasePromise.swift in Sources */,
56959629222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */,
560A37A41C8F625000949E71 /* DatabasePool.swift in Sources */,
56B7F43A1BEB42D500E39BBF /* Migration.swift in Sources */,
5613ED3521A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */,
@ -3203,11 +3248,13 @@
56FC98781D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */,
56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */,
5695311F1C907A8C00CF1A2B /* DatabaseSchemaCache.swift in Sources */,
569D6DDE220EF9E100A058A9 /* SQLInterpolation.swift in Sources */,
568D131F2207213E00674B58 /* SQLSelectQueryGenerator.swift in Sources */,
5605F15D1C672E4000235C62 /* DatabaseDateComponents.swift in Sources */,
56BF6D361DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */,
567ECE4F2222E431009245CA /* GRDB-4.0.swift in Sources */,
56193E8E1CD8A3E200F95862 /* FetchedRecordsController.swift in Sources */,
56B964B91DA51D0A0002DA19 /* FTS5Pattern.swift in Sources */,
56FBFEDA2210731A00945324 /* SQLRequest.swift in Sources */,
563363C01C942C04000BE133 /* DatabaseReader.swift in Sources */,
564CE43121AA901800652B19 /* ValueObserver.swift in Sources */,
5605F1651C672E4000235C62 /* NSNull.swift in Sources */,
@ -3215,13 +3262,14 @@
5653EB1820944C7C00F46237 /* ForeignKey.swift in Sources */,
5674A6FB1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */,
5653EB0620944C7C00F46237 /* ForeignKeyRequest.swift in Sources */,
56E9FACB221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */,
5698AC781DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */,
56A2387D1B9C75030082EB20 /* Database.swift in Sources */,
566AD8B21D5318F4002EC1A8 /* TableDefinition.swift in Sources */,
5698AD211DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */,
56A238831B9C75030082EB20 /* DatabaseQueue.swift in Sources */,
569A98ED2039B6F3008D7DBF /* Fixits-3.0.swift in Sources */,
5605F1671C672E4000235C62 /* NSNumber.swift in Sources */,
56E9FADA221053DD00C703A8 /* SQLLiteral.swift in Sources */,
C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */,
56A238871B9C75030082EB20 /* Row.swift in Sources */,
5653EB2120944C7C00F46237 /* HasOneAssociation.swift in Sources */,
@ -3242,11 +3290,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
560C97D01C0E22D300BF8471 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = "SQLite Mac";
targetProxy = 560C97D11C0E22D300BF8471 /* PBXContainerItemProxy */;
};
560C97D21C0E22D300BF8471 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DC3773F219C8CBB3004FCF85 /* GRDBOSX */;
@ -3324,7 +3367,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.2.1",
"$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-5.0",
);
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = Tests/Info.plist;
@ -3347,7 +3390,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.2.1",
"$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-5.0",
);
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = Tests/Info.plist;
@ -3620,7 +3663,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
@ -3667,7 +3710,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";

View File

@ -10,9 +10,6 @@
<FileRef
location = "group:JSONSynchronization.playground">
</FileRef>
<FileRef
location = "group:Record.playground">
</FileRef>
<FileRef
location = "group:TransactionObserver.playground">
</FileRef>

View File

@ -14,8 +14,9 @@
extension Array {
/// Creates an array containing the elements of a cursor.
///
/// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let cursor = try String.fetchCursor(db, sql: "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let strings = try Array(cursor) // ["foo", "bar"]
@inlinable
public init<C: Cursor>(_ cursor: C) throws where C.Element == Element {
self.init()
while let element = try cursor.next() {
@ -36,7 +37,7 @@ extension Sequence {
extension Set {
/// Creates a set containing the elements of a cursor.
///
/// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'foo'")
/// let cursor = try String.fetchCursor(db, sql: "SELECT 'foo' UNION ALL SELECT 'foo'")
/// let strings = try Set(cursor) // ["foo"]
public init<C: Cursor>(_ cursor: C) throws where C.Element == Element {
self.init()
@ -111,7 +112,7 @@ extension Cursor {
/// Returns a cursor of pairs (n, x), where n represents a consecutive
/// integer starting at zero, and x represents an element of the cursor.
///
/// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let cursor = try String.fetchCursor(db, sql: "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let c = cursor.enumerated()
/// while let (n, x) = c.next() {
/// print("\(n): \(x)")
@ -144,11 +145,6 @@ extension Cursor {
return map(transform).filter { $0 != nil }.map { $0! }
}
@available(*, deprecated, renamed: "compactMap")
public func flatMap<ElementOfResult>(_ transform: @escaping (Element) throws -> ElementOfResult?) -> MapCursor<FilterCursor<MapCursor<Self, ElementOfResult?>>, ElementOfResult> {
return compactMap(transform)
}
/// Returns a cursor that skips any initial elements that satisfy
/// `predicate`.
///
@ -600,7 +596,7 @@ public final class DropWhileCursor<Base: Cursor> : Cursor {
/// To create an instance of `EnumeratedCursor`, call the `enumerated()` method
/// on a cursor:
///
/// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let cursor = try String.fetchCursor(db, sql: "SELECT 'foo' UNION ALL SELECT 'bar'")
/// let c = cursor.enumerated()
/// while let (n, x) = c.next() {
/// print("\(n): \(x)")

View File

@ -171,13 +171,13 @@ extension Database {
}
let indexes = try Row
.fetchAll(self, "PRAGMA index_list(\(tableName.quotedDatabaseIdentifier))")
.fetchAll(self, sql: "PRAGMA index_list(\(tableName.quotedDatabaseIdentifier))")
.map { row -> IndexInfo in
// [seq:0 name:"index" unique:0 origin:"c" partial:0]
let indexName: String = row[1]
let unique: Bool = row[2]
let columns = try Row
.fetchAll(self, "PRAGMA index_info(\(indexName.quotedDatabaseIdentifier))")
.fetchAll(self, sql: "PRAGMA index_info(\(indexName.quotedDatabaseIdentifier))")
.map { row -> (Int, String) in
// [seqno:0 cid:2 name:"column"]
(row[0] as Int, row[2] as String)
@ -213,7 +213,7 @@ extension Database {
var rawForeignKeys: [(destinationTable: String, mapping: [(origin: String, destination: String?, seq: Int)])] = []
var previousId: Int? = nil
for row in try Row.fetchAll(self, "PRAGMA foreign_key_list(\(tableName.quotedDatabaseIdentifier))") {
for row in try Row.fetchAll(self, sql: "PRAGMA foreign_key_list(\(tableName.quotedDatabaseIdentifier))") {
// row = [id:0 seq:0 table:"parents" from:"parentId" to:"id" on_update:"..." on_delete:"..." match:"..."]
let id: Int = row[0]
let seq: Int = row[1]
@ -320,7 +320,7 @@ extension Database {
}
}
let columns = try ColumnInfo
.fetchAll(self, "PRAGMA table_info(\(tableName.quotedDatabaseIdentifier))")
.fetchAll(self, sql: "PRAGMA table_info(\(tableName.quotedDatabaseIdentifier))")
.sorted(by: { $0.cid < $1.cid })
guard columns.count > 0 else {
throw DatabaseError(message: "no such table: \(tableName)")
@ -386,7 +386,7 @@ public struct ColumnInfo : FetchableRecord {
///
/// For example:
///
/// try db.execute("""
/// try db.execute(sql: """
/// CREATE TABLE player(
/// id INTEGER PRIMARY KEY,
/// name TEXT DEFAULT 'Anonymous',
@ -579,7 +579,7 @@ struct SchemaInfo: Equatable {
private var objects: Set<SchemaObject>
init(_ db: Database) throws {
objects = try Set(SchemaObject.fetchCursor(db, """
objects = try Set(SchemaObject.fetchCursor(db, sql: """
SELECT type, name, tbl_name, sql, 0 AS isTemporary FROM sqlite_master \
UNION \
SELECT type, name, tbl_name, sql, 1 FROM sqlite_temp_master
@ -597,7 +597,7 @@ struct SchemaInfo: Equatable {
/// Returns the canonical name of the object:
///
/// try db.execute("CREATE TABLE FooBar (...)")
/// try db.execute(sql: "CREATE TABLE FooBar (...)")
/// try db.schema().canonicalName("foobar", ofType: .table) // "FooBar"
func canonicalName(_ name: String, ofType type: SchemaObjectType) -> String? {
let name = name.lowercased()
@ -610,16 +610,5 @@ struct SchemaInfo: Equatable {
var tbl_name: String?
var sql: String?
var isTemporary: Bool
#if !swift(>=4.2)
var hashValue: Int {
var hash = type.hashValue
hash ^= name.hashValue
hash ^= (tbl_name?.hashValue ?? 0)
hash ^= (sql?.hashValue ?? 0)
hash ^= isTemporary.hashValue
return hash
}
#endif
}
}

View File

@ -1,4 +1,9 @@
import Foundation
#if SWIFT_PACKAGE
import CSQLite
#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER
import SQLite3
#endif
extension Database {
@ -6,20 +11,20 @@ extension Database {
/// Returns a new prepared statement that can be reused.
///
/// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM player WHERE score > ?")
/// let statement = try db.makeSelectStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?")
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
///
/// - parameter sql: An SQL query.
/// - returns: A SelectStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
public func makeSelectStatement(_ sql: String) throws -> SelectStatement {
return try makeSelectStatement(sql, prepFlags: 0)
public func makeSelectStatement(sql: String) throws -> SelectStatement {
return try makeSelectStatement(sql: sql, prepFlags: 0)
}
/// Returns a new prepared statement that can be reused.
///
/// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM player WHERE score > ?", prepFlags: 0)
/// let statement = try db.makeSelectStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?", prepFlags: 0)
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
///
@ -28,13 +33,13 @@ extension Database {
/// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html)
/// - returns: A SelectStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
func makeSelectStatement(_ sql: String, prepFlags: Int32) throws -> SelectStatement {
func makeSelectStatement(sql: String, prepFlags: Int32) throws -> SelectStatement {
return try SelectStatement.prepare(sql: sql, prepFlags: prepFlags, in: self)
}
/// Returns a prepared statement that can be reused.
///
/// let statement = try db.cachedSelectStatement("SELECT COUNT(*) FROM player WHERE score > ?")
/// let statement = try db.cachedSelectStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?")
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
///
@ -44,31 +49,31 @@ extension Database {
/// - parameter sql: An SQL query.
/// - returns: An UpdateStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
public func cachedSelectStatement(_ sql: String) throws -> SelectStatement {
public func cachedSelectStatement(sql: String) throws -> SelectStatement {
return try publicStatementCache.selectStatement(sql)
}
/// Returns a cached statement that does not conflict with user's cached statements.
func internalCachedSelectStatement(_ sql: String) throws -> SelectStatement {
func internalCachedSelectStatement(sql: String) throws -> SelectStatement {
return try internalStatementCache.selectStatement(sql)
}
/// Returns a new prepared statement that can be reused.
///
/// let statement = try db.makeUpdateStatement("INSERT INTO player (name) VALUES (?)")
/// let statement = try db.makeUpdateStatement(sql: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
///
/// - parameter sql: An SQL query.
/// - returns: An UpdateStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
public func makeUpdateStatement(_ sql: String) throws -> UpdateStatement {
return try makeUpdateStatement(sql, prepFlags: 0)
public func makeUpdateStatement(sql: String) throws -> UpdateStatement {
return try makeUpdateStatement(sql: sql, prepFlags: 0)
}
/// Returns a new prepared statement that can be reused.
///
/// let statement = try db.makeUpdateStatement("INSERT INTO player (name) VALUES (?)", prepFlags: 0)
/// let statement = try db.makeUpdateStatement(sql: "INSERT INTO player (name) VALUES (?)", prepFlags: 0)
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
///
@ -77,13 +82,13 @@ extension Database {
/// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html)
/// - returns: An UpdateStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
func makeUpdateStatement(_ sql: String, prepFlags: Int32) throws -> UpdateStatement {
func makeUpdateStatement(sql: String, prepFlags: Int32) throws -> UpdateStatement {
return try UpdateStatement.prepare(sql: sql, prepFlags: prepFlags, in: self)
}
/// Returns a prepared statement that can be reused.
///
/// let statement = try db.cachedUpdateStatement("INSERT INTO player (name) VALUES (?)")
/// let statement = try db.cachedUpdateStatement(sql: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
///
@ -93,97 +98,121 @@ extension Database {
/// - parameter sql: An SQL query.
/// - returns: An UpdateStatement.
/// - throws: A DatabaseError whenever SQLite could not parse the sql query.
public func cachedUpdateStatement(_ sql: String) throws -> UpdateStatement {
public func cachedUpdateStatement(sql: String) throws -> UpdateStatement {
return try publicStatementCache.updateStatement(sql)
}
/// Returns a cached statement that does not conflict with user's cached statements.
func internalCachedUpdateStatement(_ sql: String) throws -> UpdateStatement {
func internalCachedUpdateStatement(sql: String) throws -> UpdateStatement {
return try internalStatementCache.updateStatement(sql)
}
/// Executes one or several SQL statements, separated by semi-colons.
///
/// try db.execute(
/// "INSERT INTO player (name) VALUES (:name)",
/// sql: "INSERT INTO player (name) VALUES (:name)",
/// arguments: ["name": "Arthur"])
///
/// try db.execute("""
/// try db.execute(sql: """
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// """, arguments; ['Arthur', 'Barbara', 'Craig'])
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
///
/// This method may throw a DatabaseError.
///
/// - parameters:
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func execute(_ sql: String, arguments: StatementArguments? = nil) throws {
public func execute(sql: String, arguments: StatementArguments = StatementArguments()) throws {
try execute(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Executes one or several SQL statements, separated by semi-colons.
///
/// try db.execute(literal: SQLLiteral(
/// sql: "INSERT INTO player (name) VALUES (:name)",
/// arguments: ["name": "Arthur"]))
///
/// try db.execute(literal: SQLLiteral(sql: """
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// """, arguments: ["Arthur", "Barbara", "O'Brien"]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// try db.execute(literal: """
/// INSERT INTO player (name) VALUES (\("Arthur"));
/// INSERT INTO player (name) VALUES (\("Barbara"));
/// INSERT INTO player (name) VALUES (\("O'Brien"));
/// """)
///
/// This method may throw a DatabaseError.
///
/// - parameter sqlLiteral: An SQLLiteral.
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func execute(literal sqlLiteral: SQLLiteral) throws {
// This method is like sqlite3_exec (https://www.sqlite.org/c3ref/exec.html)
// It adds support for arguments, and the tricky part is to consume
// arguments as statements are executed.
//
// Here we build two functions:
// - consumeArguments returns arguments for a statement
// - validateRemainingArguments validates the remaining arguments, after
// all statements have been executed, in the same way
// as Statement.validate(arguments:)
// This job is performed by StatementArguments.extractBindings(forStatement:allowingRemainingValues:)
//
// And before we return, we'll check that all arguments were consumed.
var arguments = arguments ?? StatementArguments()
var arguments = sqlLiteral.arguments
let initialValuesCount = arguments.values.count
let consumeArguments = { (statement: UpdateStatement) throws -> StatementArguments in
let bindings = try arguments.consume(statement, allowingRemainingValues: true)
return StatementArguments(bindings)
}
let validateRemainingArguments = {
if !arguments.values.isEmpty {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "wrong number of statement arguments: \(initialValuesCount)")
}
}
// Iterate SQL statements
let sqlCodeUnits = sql.utf8CString
try sqlCodeUnits.withUnsafeBufferPointer { codeUnits in
let sqlStart = UnsafePointer<Int8>(codeUnits.baseAddress)!
let sqlEnd = sqlStart + sqlCodeUnits.count
// Build a C string (SQLite wants that), and execute SQL statements one
// after the other.
try sqlLiteral.sql.utf8CString.withUnsafeBufferPointer { buffer in
guard let sqlStart = buffer.baseAddress else { return }
let sqlEnd = sqlStart + buffer.count // past \0
var statementStart = sqlStart
while statementStart < sqlEnd - 1 {
while statementStart < sqlEnd {
var statementEnd: UnsafePointer<Int8>? = nil
let nextStatement: UpdateStatement?
// Compile
do {
let statement: UpdateStatement
// Compile
do {
let statementCompilationAuthorizer = StatementCompilationAuthorizer()
authorizer = statementCompilationAuthorizer
defer { authorizer = nil }
statement = try UpdateStatement(
database: self,
statementStart: statementStart,
statementEnd: &statementEnd,
prepFlags: 0,
authorizer: statementCompilationAuthorizer)
}
let statementCompilationAuthorizer = StatementCompilationAuthorizer()
authorizer = statementCompilationAuthorizer
defer { authorizer = nil }
// Execute
let arguments = try consumeArguments(statement)
statement.unsafeSetArguments(arguments)
try statement.execute()
// Next
statementStart = statementEnd!
} catch is EmptyStatementError {
// End
nextStatement = try UpdateStatement(
database: self,
statementStart: statementStart,
statementEnd: &statementEnd,
prepFlags: 0,
authorizer: statementCompilationAuthorizer)
}
guard let statement = nextStatement else {
// End of SQL string
break
}
// Extract statement arguments
let bindings = try arguments.extractBindings(forStatement: statement, allowingRemainingValues: true)
// unsafe is OK because we just extracted the correct number of arguments
statement.unsafeSetArguments(StatementArguments(bindings))
// Execute
try statement.execute()
// Next
statementStart = statementEnd!
}
}
// Force arguments validity: it is a programmer error to provide
// arguments that do not match the statement.
try! validateRemainingArguments() // throws if there are remaining arguments.
// Check that all arguments were consumed: it is a programmer error to
// provide arguments that do not match the statement.
if arguments.values.isEmpty == false {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "wrong number of statement arguments: \(initialValuesCount)")
}
}
}
@ -248,12 +277,17 @@ struct StatementCache {
//
// However SQLITE_PREPARE_PERSISTENT was only introduced in
// SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
//
// TODO: use SQLITE_PREPARE_PERSISTENT if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *)
#if GRDBCUSTOMSQLITE || GRDBCIPHER
let statement = try db.makeSelectStatement(sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
let statement = try db.makeSelectStatement(sql: sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
#else
let statement = try db.makeSelectStatement(sql)
let statement: SelectStatement
if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
// SQLite 3.24.0 or more
statement = try db.makeSelectStatement(sql: sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
} else {
// SQLite 3.19.3 or less
statement = try db.makeSelectStatement(sql: sql)
}
#endif
selectStatements[sql] = statement
return statement
@ -273,12 +307,17 @@ struct StatementCache {
//
// However SQLITE_PREPARE_PERSISTENT was only introduced in
// SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
//
// TODO: use SQLITE_PREPARE_PERSISTENT if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *)
#if GRDBCUSTOMSQLITE || GRDBCIPHER
let statement = try db.makeUpdateStatement(sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
let statement = try db.makeUpdateStatement(sql: sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
#else
let statement = try db.makeUpdateStatement(sql)
let statement: UpdateStatement
if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
// SQLite 3.24.0 or more
statement = try db.makeUpdateStatement(sql: sql, prepFlags: SQLITE_PREPARE_PERSISTENT)
} else {
// SQLite 3.19.3 or less
statement = try db.makeUpdateStatement(sql: sql)
}
#endif
updateStatements[sql] = statement
return statement
@ -290,14 +329,10 @@ struct StatementCache {
}
mutating func remove(_ statement: SelectStatement) {
if let index = selectStatements.index(where: { $0.1 === statement }) {
selectStatements.remove(at: index)
}
selectStatements.removeFirst { $0.value === statement }
}
mutating func remove(_ statement: UpdateStatement) {
if let index = updateStatements.index(where: { $0.1 === statement }) {
updateStatements.remove(at: index)
}
updateStatements.removeFirst { $0.value === statement }
}
}

View File

@ -9,7 +9,7 @@ import Foundation
public typealias SQLiteConnection = OpaquePointer
/// A raw SQLite function argument.
typealias SQLiteValue = OpaquePointer
@usableFromInline typealias SQLiteValue = OpaquePointer
let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self)
@ -151,7 +151,7 @@ public final class Database {
/// The list of compile options used when building SQLite
static let sqliteCompileOptions: Set<String> = DatabaseQueue().inDatabase {
try! Set(String.fetchCursor($0, "PRAGMA COMPILE_OPTIONS"))
try! Set(String.fetchCursor($0, sql: "PRAGMA COMPILE_OPTIONS"))
}
// MARK: - Private properties
@ -256,9 +256,15 @@ extension Database {
private static func set(passphrase: String, forConnection sqliteConnection: SQLiteConnection) throws {
let data = passphrase.data(using: .utf8)!
let code = data.withUnsafeBytes { bytes in
sqlite3_key(sqliteConnection, bytes, Int32(data.count))
#if swift(>=5.0)
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0.baseAddress, Int32($0.count))
}
#else
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0, Int32(data.count))
}
#endif
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
@ -379,7 +385,7 @@ extension Database {
private func setupForeignKeys() throws {
// Foreign keys are disabled by default with SQLite3
if configuration.foreignKeysEnabled {
try execute("PRAGMA foreign_keys = ON")
try execute(sql: "PRAGMA foreign_keys = ON")
}
}
@ -411,7 +417,7 @@ extension Database {
add(function: .lowercase)
add(function: .uppercase)
if #available(iOS 9.0, OSX 10.11, watchOS 3.0, *) {
if #available(OSX 10.11, watchOS 3.0, *) {
add(function: .localizedCapitalize)
add(function: .localizedLowercase)
add(function: .localizedUppercase)
@ -450,7 +456,7 @@ extension Database {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
closeConnection_v2(sqliteConnection, sqlite3_close_v2)
#else
if #available(iOS 8.2, OSX 10.10, OSXApplicationExtension 10.10, *) {
if #available(OSX 10.10, OSXApplicationExtension 10.10, *) {
closeConnection_v2(sqliteConnection, sqlite3_close_v2)
} else {
closeConnection_v1(sqliteConnection)
@ -515,7 +521,7 @@ extension Database {
/// return int + 1
/// }
/// db.add(function: fn)
/// try Int.fetchOne(db, "SELECT succ(1)")! // 2
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
public func add(function: DatabaseFunction) {
functions.update(with: function)
function.install(in: self)
@ -538,7 +544,7 @@ extension Database {
/// return (string1 as NSString).localizedStandardCompare(string2)
/// }
/// db.add(collation: collation)
/// try db.execute("CREATE TABLE files (name TEXT COLLATE localized_standard")
/// try db.execute(sql: "CREATE TABLE files (name TEXT COLLATE localized_standard")
public func add(collation: DatabaseCollation) {
collations.update(with: collation)
let collationPointer = Unmanaged.passUnretained(collation).toOpaque()
@ -581,8 +587,8 @@ extension Database {
// query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
// Assume those pragmas never fail
try! internalCachedUpdateStatement("PRAGMA query_only = 1").execute()
defer { try! internalCachedUpdateStatement("PRAGMA query_only = 0").execute() }
try! internalCachedUpdateStatement(sql: "PRAGMA query_only = 1").execute()
defer { try! internalCachedUpdateStatement(sql: "PRAGMA query_only = 0").execute() }
return try block()
}
}
@ -595,7 +601,7 @@ extension Database {
///
/// try dbQueue.inDatabase do {
/// try db.inTransaction {
/// try db.execute("INSERT ...")
/// try db.execute(sql: "INSERT ...")
/// return .commit
/// }
/// }
@ -655,7 +661,7 @@ extension Database {
///
/// try dbQueue.inDatabase do {
/// try db.inSavepoint {
/// try db.execute("INSERT ...")
/// try db.execute(sql: "INSERT ...")
/// return .commit
/// }
/// }
@ -691,7 +697,7 @@ extension Database {
// using unique savepoint names. User could still mess with them
// with raw SQL queries, but let's assume that it is unlikely that
// the user uses "grdb" as a savepoint name.
try execute("SAVEPOINT grdb")
try execute(sql: "SAVEPOINT grdb")
// Now that savepoint has begun, we'll rollback in case of error.
// But we'll throw the first caught error, so that user knows
@ -702,7 +708,7 @@ extension Database {
let completion = try block()
switch completion {
case .commit:
try execute("RELEASE SAVEPOINT grdb")
try execute(sql: "RELEASE SAVEPOINT grdb")
assert(!topLevelSavepoint || !isInsideTransaction)
needsRollback = false
case .rollback:
@ -721,8 +727,8 @@ extension Database {
// Rollback, and release the savepoint.
// Rollback alone is not enough to clear the savepoint from
// the SQLite savepoint stack.
try execute("ROLLBACK TRANSACTION TO SAVEPOINT grdb")
try execute("RELEASE SAVEPOINT grdb")
try execute(sql: "ROLLBACK TRANSACTION TO SAVEPOINT grdb")
try execute(sql: "RELEASE SAVEPOINT grdb")
}
} catch {
if firstError == nil {
@ -745,7 +751,7 @@ extension Database {
/// - throws: The error thrown by the block.
public func beginTransaction(_ kind: TransactionKind? = nil) throws {
let kind = kind ?? configuration.defaultTransactionKind
try execute("BEGIN \(kind.rawValue) TRANSACTION")
try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION")
assert(isInsideTransaction)
}
@ -795,7 +801,7 @@ extension Database {
// UPDATE ...
// Here the change is not visible by GRDB user
try beginTransaction(.deferred)
try internalCachedSelectStatement("SELECT rootpage FROM sqlite_master LIMIT 1").makeCursor().next()
try internalCachedSelectStatement(sql: "SELECT rootpage FROM sqlite_master LIMIT 1").makeCursor().next()
}
/// Rollbacks a database transaction.
@ -841,14 +847,14 @@ extension Database {
// should be exposed to the library user.
SchedulingWatchdog.preconditionValidQueue(self) // guard sqlite3_get_autocommit
if sqlite3_get_autocommit(sqliteConnection) == 0 {
try execute("ROLLBACK TRANSACTION")
try execute(sql: "ROLLBACK TRANSACTION")
}
assert(!isInsideTransaction)
}
/// Commits a database transaction.
public func commit() throws {
try execute("COMMIT TRANSACTION")
try execute(sql: "COMMIT TRANSACTION")
assert(!isInsideTransaction)
}
}
@ -924,9 +930,15 @@ extension Database {
// > schema of the original db into the new one:
// > https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/
let data = passphrase.data(using: .utf8)!
let code = data.withUnsafeBytes { bytes in
sqlite3_rekey(sqliteConnection, bytes, Int32(data.count))
#if swift(>=5.0)
let code = data.withUnsafeBytes {
sqlite3_rekey(sqliteConnection, $0.baseAddress, Int32($0.count))
}
#else
let code = data.withUnsafeBytes {
sqlite3_rekey(sqliteConnection, $0, Int32(data.count))
}
#endif
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: lastErrorMessage)
}

View File

@ -16,7 +16,7 @@ public final class DatabaseCollation {
/// return (string1 as NSString).localizedStandardCompare(string2)
/// }
/// db.add(collation: collation)
/// try db.execute("CREATE TABLE file (name TEXT COLLATE localized_standard")
/// try db.execute(sql: "CREATE TABLE file (name TEXT COLLATE localized_standard")
///
/// - parameters:
/// - name: The function name.
@ -39,17 +39,10 @@ extension DatabaseCollation: Hashable {
// implies hash equality) is thus non trivial. But it's not that
// important, since this hashValue is only used when one adds
// or removes a collation from a database connection.
#if swift(>=4.2)
/// :nodoc:
public func hash(into hasher: inout Hasher) {
hasher.combine(0)
}
#else
/// :nodoc:
public var hashValue: Int {
return 0
}
#endif
/// Two collations are equal if they share the same name (case insensitive)
/// :nodoc:

View File

@ -147,7 +147,7 @@ extension ResultCode {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
return String(cString: sqlite3_errstr(rawValue))
#else
if #available(iOS 8.2, OSX 10.10, OSXApplicationExtension 10.10, iOSApplicationExtension 8.2, *) {
if #available(OSX 10.10, OSXApplicationExtension 10.10, *) {
return String(cString: sqlite3_errstr(rawValue))
} else {
return nil

View File

@ -27,7 +27,7 @@ public final class DatabaseFunction: Hashable {
/// return int + 1
/// }
/// db.add(function: fn)
/// try Int.fetchOne(db, "SELECT succ(1)")! // 2
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
///
/// - parameters:
/// - name: The function name.
@ -73,10 +73,10 @@ public final class DatabaseFunction: Hashable {
/// let fn = DatabaseFunction("mysum", argumentCount: 1, aggregate: MySum.self)
/// dbQueue.add(function: fn)
/// try dbQueue.write { db in
/// try db.execute("CREATE TABLE test(i)")
/// try db.execute("INSERT INTO test(i) VALUES (1)")
/// try db.execute("INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, "SELECT mysum(i) FROM test")! // 3
/// try db.execute(sql: "CREATE TABLE test(i)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, sql: "SELECT mysum(i) FROM test")! // 3
/// }
///
/// - parameters:
@ -262,7 +262,7 @@ public final class DatabaseFunction: Hashable {
let stride = MemoryLayout<Unmanaged<AggregateContext>>.stride
let aggregateContextBufferP = UnsafeMutableRawBufferPointer(start: sqlite3_aggregate_context(sqliteContext, Int32(stride))!, count: stride)
if aggregateContextBufferP.contains(where: { $0 != 0 }) {
if aggregateContextBufferP.contains(where: { $0 != 0 }) { // TODO: This testt looks weird. Review.
// Buffer contains non-null pointer: load aggregate context
let aggregateContextP = aggregateContextBufferP.baseAddress!.assumingMemoryBound(to: Unmanaged<AggregateContext>.self)
return aggregateContextP.pointee
@ -275,8 +275,8 @@ public final class DatabaseFunction: Hashable {
// retain and store in SQLite's buffer
let aggregateContextU = Unmanaged.passRetained(aggregateContext)
var aggregateContextP = aggregateContextU.toOpaque()
withUnsafeBytes(of: &aggregateContextP) {
let aggregateContextP = aggregateContextU.toOpaque()
withUnsafeBytes(of: aggregateContextP) {
aggregateContextBufferP.copyMemory(from: $0)
}
return aggregateContextU
@ -294,9 +294,15 @@ public final class DatabaseFunction: Hashable {
case .string(let string):
sqlite3_result_text(sqliteContext, string, -1, SQLITE_TRANSIENT)
case .blob(let data):
data.withUnsafeBytes { bytes in
sqlite3_result_blob(sqliteContext, bytes, Int32(data.count), SQLITE_TRANSIENT)
#if swift(>=5.0)
data.withUnsafeBytes {
sqlite3_result_blob(sqliteContext, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
}
#else
data.withUnsafeBytes {
sqlite3_result_blob(sqliteContext, $0, Int32(data.count), SQLITE_TRANSIENT)
}
#endif
}
}
@ -313,17 +319,10 @@ public final class DatabaseFunction: Hashable {
}
extension DatabaseFunction {
#if swift(>=4.2)
/// :nodoc:
public func hash(into hasher: inout Hasher) {
hasher.combine(identity)
}
#else
/// :nodoc:
public var hashValue: Int {
return identity.hashValue
}
#endif
/// Two functions are equal if they share the same name and arity.
/// :nodoc:
@ -354,10 +353,10 @@ extension DatabaseFunction {
/// let fn = DatabaseFunction("mysum", argumentCount: 1, aggregate: MySum.self)
/// dbQueue.add(function: fn)
/// try dbQueue.write { db in
/// try db.execute("CREATE TABLE test(i)")
/// try db.execute("INSERT INTO test(i) VALUES (1)")
/// try db.execute("INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, "SELECT mysum(i) FROM test")! // 3
/// try db.execute(sql: "CREATE TABLE test(i)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, sql: "SELECT mysum(i) FROM test")! // 3
/// }
public protocol DatabaseAggregate {
/// Creates an aggregate.

View File

@ -58,14 +58,14 @@ public final class DatabasePool: DatabaseWriter {
// Activate WAL Mode unless readonly
if !configuration.readonly {
try writer.sync { db in
let journalMode = try String.fetchOne(db, "PRAGMA journal_mode = WAL")
let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode = WAL")
guard journalMode == "wal" else {
throw DatabaseError(message: "could not activate WAL Mode at path: \(path)")
}
// https://www.sqlite.org/pragma.html#pragma_synchronous
// > Many applications choose NORMAL when in WAL mode
try db.execute("PRAGMA synchronous = NORMAL")
try db.execute(sql: "PRAGMA synchronous = NORMAL")
if !FileManager.default.fileExists(atPath: path + "-wal") {
// Create the -wal file if it does not exist yet. This
@ -73,7 +73,7 @@ public final class DatabasePool: DatabaseWriter {
// opens a pool to an existing non-WAL database, and
// attempts to read from it.
// See https://github.com/groue/GRDB.swift/issues/102
try db.execute("CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY); DROP TABLE grdb_issue_102;")
try db.execute(sql: "CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY); DROP TABLE grdb_issue_102;")
}
}
}
@ -177,13 +177,16 @@ extension DatabasePool {
public func setupMemoryManagement(in application: UIApplication) {
self.application = application
let center = NotificationCenter.default
#if swift(>=4.2)
center.addObserver(self, selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
center.addObserver(self, selector: #selector(DatabasePool.applicationDidEnterBackground(_:)), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
#else
center.addObserver(self, selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)), name: .UIApplicationDidReceiveMemoryWarning, object: nil)
center.addObserver(self, selector: #selector(DatabasePool.applicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: nil)
#endif
center.addObserver(
self,
selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
center.addObserver(
self,
selector: #selector(DatabasePool.applicationDidEnterBackground(_:)),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
}
@objc private func applicationDidEnterBackground(_ notification: NSNotification) {
@ -192,12 +195,7 @@ extension DatabasePool {
}
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
#if swift(>=4.2)
let taskIsInvalid = task == UIBackgroundTaskIdentifier.invalid
#else
let taskIsInvalid = task == UIBackgroundTaskInvalid
#endif
if taskIsInvalid {
if task == .invalid {
// Perform releaseMemory() synchronously.
releaseMemory()
} else {
@ -249,13 +247,13 @@ extension DatabasePool : DatabaseReader {
/// try dbPool.read { db in
/// // Those two values are guaranteed to be equal, even if the
/// // `wine` table is modified between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// try dbPool.read { db in
/// // Now this value may be different:
/// let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// This method is *not* reentrant.
@ -290,15 +288,15 @@ extension DatabasePool : DatabaseReader {
/// try dbPool.unsafeRead { db in
/// // Those two values may be different because some other thread
/// // may have inserted or deleted a wine between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// Cursor iteration is safe, though:
///
/// try dbPool.unsafeRead { db in
/// // No concurrent update can mess with this iteration:
/// let rows = try Row.fetchCursor(db, "SELECT ...")
/// let rows = try Row.fetchCursor(db, sql: "SELECT ...")
/// while let row = try rows.next() { ... }
/// }
///
@ -327,15 +325,15 @@ extension DatabasePool : DatabaseReader {
/// try dbPool.unsafeReentrantRead { db in
/// // Those two values may be different because some other thread
/// // may have inserted or deleted a wine between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// Cursor iteration is safe, though:
///
/// try dbPool.unsafeReentrantRead { db in
/// // No concurrent update can mess with this iteration:
/// let rows = try Row.fetchCursor(db, "SELECT ...")
/// let rows = try Row.fetchCursor(db, sql: "SELECT ...")
/// while let row = try rows.next() { ... }
/// }
///
@ -359,80 +357,7 @@ extension DatabasePool : DatabaseReader {
}
}
/// This method is deprecated. Use concurrentRead instead.
///
/// Asynchronously executes a read-only block in a protected dispatch queue,
/// wrapped in a deferred transaction.
///
/// This method must be called from the writing dispatch queue, outside of a
/// transaction. You'll get a fatal error otherwise.
///
/// The *block* argument is guaranteed to see the database in the last
/// committed state at the moment this method is called. Eventual concurrent
/// database updates are *not visible* inside the block.
///
/// try dbPool.write { db in
/// try db.execute("DELETE FROM player")
/// try dbPool.readFromCurrentState { db in
/// // Guaranteed to be zero
/// try Int.fetchOne(db, "SELECT COUNT(*) FROM player")!
/// }
/// try db.execute("INSERT INTO player ...")
/// }
///
/// This method blocks the current thread until the isolation guarantee has
/// been established, and before the block argument has run.
///
/// - parameter block: A block that accesses the database.
/// - throws: The error thrown by the block, or any DatabaseError that would
/// happen while establishing the read access to the database.
@available(*, deprecated, message: "Use concurrentRead instead")
public func readFromCurrentState(_ block: @escaping (Database) -> Void) throws {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
GRDBPrecondition(!db.isInsideTransaction, """
readFromCurrentState must not be called from inside a transaction. \
If this error is raised from a DatabasePool.write block, use \
DatabasePool.writeWithoutTransaction instead (and use \
transactions when needed).
""")
}
// The semaphore that blocks the writing dispatch queue until snapshot
// isolation has been established:
let semaphore = DispatchSemaphore(value: 0)
var snapshotIsolationError: Error? = nil
let (reader, releaseReader) = try readerPool.get()
reader.async { db in
defer {
_ = try? db.commit() // Ignore commit error
releaseReader()
}
do {
try db.beginSnapshotIsolation()
} catch {
snapshotIsolationError = error
semaphore.signal() // Release the writer queue and rethrow error
return
}
semaphore.signal() // We can release the writer queue now that we are isolated for good
// Reset the schema cache before running user code in snapshot isolation
db.schemaCache = SimpleDatabaseSchemaCache()
block(db)
}
_ = semaphore.wait(timeout: .distantFuture)
if let error = snapshotIsolationError {
// TODO: write a test for this
throw error
}
}
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> Future<T> {
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
@ -479,16 +404,16 @@ extension DatabasePool : DatabaseReader {
futureSemaphore.signal()
}
} catch {
return Future { throw error }
return DatabaseFuture { throw error }
}
// Block the writer queue until snapshot isolation success or error
_ = isolationSemaphore.wait(timeout: .distantFuture)
return Future {
return DatabaseFuture {
// Block the future until results are fetched
_ = futureSemaphore.wait(timeout: .distantFuture)
return try futureResult!.unwrap()
return try futureResult!.get()
}
}
@ -632,7 +557,7 @@ extension DatabasePool : DatabaseReader {
/// }
/// dbPool.add(function: fn)
/// try dbPool.read { db in
/// try Int.fetchOne(db, "SELECT succ(1)") // 2
/// try Int.fetchOne(db, sql: "SELECT succ(1)") // 2
/// }
public func add(function: DatabaseFunction) {
functions.update(with: function)
@ -654,7 +579,7 @@ extension DatabasePool : DatabaseReader {
/// }
/// dbPool.add(collation: collation)
/// try dbPool.write { db in
/// try db.execute("CREATE TABLE file (name TEXT COLLATE LOCALIZED_STANDARD")
/// try db.execute(sql: "CREATE TABLE file (name TEXT COLLATE LOCALIZED_STANDARD")
/// }
public func add(collation: DatabaseCollation) {
collations.update(with: collation)

View File

@ -94,13 +94,16 @@ extension DatabaseQueue {
public func setupMemoryManagement(in application: UIApplication) {
self.application = application
let center = NotificationCenter.default
#if swift(>=4.2)
center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
#else
center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)), name: .UIApplicationDidReceiveMemoryWarning, object: nil)
center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: nil)
#endif
center.addObserver(
self,
selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
center.addObserver(
self,
selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
}
@objc private func applicationDidEnterBackground(_ notification: NSNotification) {
@ -109,12 +112,7 @@ extension DatabaseQueue {
}
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
#if swift(>=4.2)
let taskIsInvalid = task == UIBackgroundTaskIdentifier.invalid
#else
let taskIsInvalid = task == UIBackgroundTaskInvalid
#endif
if taskIsInvalid {
if task == .invalid {
// Perform releaseMemory() synchronously.
releaseMemory()
} else {
@ -203,30 +201,10 @@ extension DatabaseQueue {
return try writer.reentrantSync(block)
}
/// This method is deprecated. Use concurrentRead instead.
///
/// Synchronously executes *block*.
///
/// This method must be called from the protected database dispatch queue,
/// outside of a transaction. You'll get a fatal error otherwise.
///
/// Starting SQLite 3.8.0 (iOS 8.2+, OSX 10.10+, custom SQLite builds and
/// SQLCipher), attempts to write in the database from this meethod throw a
/// DatabaseError of resultCode `SQLITE_READONLY`.
@available(*, deprecated, message: "Use concurrentRead instead")
public func readFromCurrentState(_ block: @escaping (Database) -> Void) {
// Check that we're on the correct queue...
writer.execute { db in
// ... and that no transaction is opened.
GRDBPrecondition(!db.isInsideTransaction, "readFromCurrentState must not be called from inside a transaction.")
db.readOnly { block(db) }
}
}
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> Future<T> {
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// DatabaseQueue can't perform parallel reads.
// Perform a blocking read instead.
return Future(Result {
return DatabaseFuture(Result {
// Check that we're on the writer queue...
try writer.execute { db in
// ... and that no transaction is opened.
@ -354,7 +332,7 @@ extension DatabaseQueue {
/// }
/// dbQueue.add(function: fn)
/// try dbQueue.read { db in
/// try Int.fetchOne(db, "SELECT succ(1)") // 2
/// try Int.fetchOne(db, sql: "SELECT succ(1)") // 2
/// }
public func add(function: DatabaseFunction) {
writer.sync { $0.add(function: function) }
@ -374,7 +352,7 @@ extension DatabaseQueue {
/// }
/// dbQueue.add(collation: collation)
/// try dbQueue.write { db in
/// try db.execute("CREATE TABLE file (name TEXT COLLATE LOCALIZED_STANDARD")
/// try db.execute(sql: "CREATE TABLE file (name TEXT COLLATE LOCALIZED_STANDARD")
/// }
public func add(collation: DatabaseCollation) {
writer.sync { $0.add(collation: collation) }

View File

@ -32,13 +32,13 @@ public protocol DatabaseReader : class {
/// try reader.read { db in
/// // Those two values are guaranteed to be equal, even if the
/// // `wine` table is modified between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// try reader.read { db in
/// // Now this value may be different:
/// let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// Guarantee 2: Starting iOS 8.2, OSX 10.10, and with custom SQLite builds
@ -61,15 +61,15 @@ public protocol DatabaseReader : class {
/// try reader.unsafeRead { db in
/// // Those two values may be different because some other thread
/// // may have inserted or deleted a wine between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// Cursor iterations are isolated, though:
///
/// try reader.unsafeRead { db in
/// // No concurrent update can mess with this iteration:
/// let rows = try Row.fetchCursor(db, "SELECT ...")
/// let rows = try Row.fetchCursor(db, sql: "SELECT ...")
/// while let row = try rows.next() { ... }
/// }
///
@ -92,15 +92,15 @@ public protocol DatabaseReader : class {
/// try reader.unsafeReentrantRead { db in
/// // Those two values may be different because some other thread
/// // may have inserted or deleted a wine between the two requests:
/// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wine")!
/// let count1 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// let count2 = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM wine")!
/// }
///
/// Cursor iterations are isolated, though:
///
/// try reader.unsafeReentrantRead { db in
/// // No concurrent update can mess with this iteration:
/// let rows = try Row.fetchCursor(db, "SELECT ...")
/// let rows = try Row.fetchCursor(db, sql: "SELECT ...")
/// while let row = try rows.next() { ... }
/// }
///
@ -128,7 +128,7 @@ public protocol DatabaseReader : class {
/// }
/// reader.add(function: fn)
/// try reader.read { db in
/// try Int.fetchOne(db, "SELECT succ(1)")! // 2
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
/// }
func add(function: DatabaseFunction)
@ -144,7 +144,7 @@ public protocol DatabaseReader : class {
/// return (string1 as NSString).localizedStandardCompare(string2)
/// }
/// reader.add(collation: collation)
/// try reader.execute("SELECT * FROM file ORDER BY name COLLATE localized_standard")
/// try reader.execute(sql: "SELECT * FROM file ORDER BY name COLLATE localized_standard")
func add(collation: DatabaseCollation)
/// Remove a collation.

View File

@ -18,7 +18,7 @@
///
/// - `SelectStatement.databaseRegion`:
///
/// let statement = db.makeSelectStatement("SELECT name, score FROM player")
/// let statement = db.makeSelectStatement(sql: "SELECT name, score FROM player")
/// print(statement.databaseRegion)
/// // prints "player(name,score)"
///
@ -33,7 +33,7 @@
/// don't know about rowids:
///
/// // A plain statement
/// let statement = db.makeSelectStatement("SELECT * FROM player WHERE id = 1")
/// let statement = db.makeSelectStatement(sql: "SELECT * FROM player WHERE id = 1")
/// statement.databaseRegion // "player(*)"
///
/// // A query interface request that executes the same statement:
@ -302,7 +302,6 @@ private struct TableRegion: Equatable {
return TableRegion(columns: columnsUnion, rowIds: rowIdsUnion)
}
@inline(__always)
func contains(rowID: Int64) -> Bool {
guard let rowIds = rowIds else {
return true

View File

@ -28,7 +28,7 @@ public class DatabaseSnapshot : DatabaseReader {
try serializedDatabase.sync { db in
// Assert WAL mode
let journalMode = try String.fetchOne(db, "PRAGMA journal_mode")
let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode")
guard journalMode == "wal" else {
throw DatabaseError(message: "WAL mode is not activated at path: \(path)")
}
@ -116,7 +116,7 @@ extension DatabaseSnapshot {
}
}
}
case let .onQueue(queue, startImmediately: startImmediately):
case let .async(onQueue: queue, startImmediately: startImmediately):
if startImmediately {
if let value = try unsafeReentrantRead(observation.initialValue) {
queue.async {

View File

@ -96,6 +96,7 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon
}
// SQLite function argument
@usableFromInline
init(sqliteValue: SQLiteValue) {
switch sqlite3_value_type(sqliteValue) {
case SQLITE_NULL:
@ -120,6 +121,7 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon
}
/// Returns a DatabaseValue initialized from a raw SQLite statement pointer.
@usableFromInline
init(sqliteStatement: SQLiteStatement, index: Int32) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
@ -149,7 +151,6 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon
// Hashable
extension DatabaseValue {
#if swift(>=4.2)
/// :nodoc:
public func hash(into hasher: inout Hasher) {
switch storage {
@ -166,24 +167,6 @@ extension DatabaseValue {
hasher.combine(data)
}
}
#else
/// :nodoc:
public var hashValue: Int {
switch storage {
case .null:
return 0
case .int64(let int64):
// 1 == 1.0, hence 1 and 1.0 must have the same hash:
return Double(int64).hashValue
case .double(let double):
return double.hashValue
case .string(let string):
return string.hashValue
case .blob(let data):
return data.hashValue
}
}
#endif
/// Returns whether two DatabaseValues are equal.
///
@ -222,57 +205,6 @@ extension DatabaseValue {
}
}
// MARK: - Lossless conversions
extension DatabaseValue {
// TODO: deprecate and rename to DatabaseValue.decode(_:sql:arguments:)
/// Converts the database value to the type T.
///
/// let dbValue = "foo".databaseValue
/// let string = dbValue.losslessConvert() as String // "foo"
///
/// Conversion is successful if and only if T.fromDatabaseValue returns a
/// non-nil value.
///
/// This method crashes with a fatal error when conversion fails.
///
/// let dbValue = "foo".databaseValue
/// let int = dbValue.losslessConvert() as Int // fatalError
///
/// - parameters:
/// - sql: Optional SQL statement that enhances the eventual
/// conversion error
/// - arguments: Optional statement arguments that enhances the eventual
/// conversion error
public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible {
return T.decode(from: self, conversionContext: sql.map { ValueConversionContext(sql: $0, arguments: arguments) })
}
// TODO: deprecate and rename to DatabaseValue.decodeIfPresent(_:sql:arguments:)
/// Converts the database value to the type Optional<T>.
///
/// let dbValue = "foo".databaseValue
/// let string = dbValue.losslessConvert() as String? // "foo"
/// let null = DatabaseValue.null.losslessConvert() as String? // nil
///
/// Conversion is successful if and only if T.fromDatabaseValue returns a
/// non-nil value.
///
/// This method crashes with a fatal error when conversion fails.
///
/// let dbValue = "foo".databaseValue
/// let int = dbValue.losslessConvert() as Int? // fatalError
///
/// - parameters:
/// - sql: Optional SQL statement that enhances the eventual
/// conversion error
/// - arguments: Optional statement arguments that enhances the eventual
/// conversion error
public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible {
return T.decodeIfPresent(from: self, conversionContext: sql.map { ValueConversionContext(sql: $0, arguments: arguments) })
}
}
// DatabaseValueConvertible
extension DatabaseValue {
/// Returns self
@ -305,12 +237,12 @@ extension DatabaseValue {
return "NULL"
}
if context.appendArguments([self]) {
if context.append(arguments: [self]) {
return "?"
} else {
// Correctness above all: use SQLite to quote the value.
// Assume that the Quote function always succeeds
return DatabaseQueue().inDatabase { try! String.fetchOne($0, "SELECT QUOTE(?)", arguments: [self])! }
return DatabaseQueue().inDatabase { try! String.fetchOne($0, sql: "SELECT QUOTE(?)", arguments: [self])! }
}
}

View File

@ -68,7 +68,7 @@ extension ValueConversionContext {
arguments: statement.arguments,
column: nil)
} else if let sqliteStatement = row.sqliteStatement {
let sql = String(cString: sqlite3_sql(sqliteStatement)).trimmingCharacters(in: statementSeparatorCharacterSet)
let sql = String(cString: sqlite3_sql(sqliteStatement)).trimmingCharacters(in: .sqlStatementSeparators)
self.init(
row: row.copy(),
sql: sql,
@ -82,14 +82,6 @@ extension ValueConversionContext {
column: nil)
}
}
init(sql: String, arguments: StatementArguments?) {
self.init(
row: nil,
sql: sql,
arguments: arguments,
column: nil)
}
}
/// The canonical conversion error message
@ -138,7 +130,16 @@ func fatalConversionError<T>(to: T.Type, from dbValue: DatabaseValue?, conversio
fatalError(conversionErrorMessage(to: T.self, from: dbValue, conversionContext: conversionContext), file: file, line: line)
}
func fatalConversionError<T>(to: T.Type, sqliteStatement: SQLiteStatement, index: Int32) -> Never {
@usableFromInline
func fatalConversionError<T>(to: T.Type, from dbValue: DatabaseValue?, in row: Row, atColumn columnName: String, file: StaticString = #file, line: UInt = #line) -> Never {
fatalConversionError(
to: T.self,
from: dbValue,
conversionContext: ValueConversionContext(row).atColumn(columnName))
}
@usableFromInline
func fatalConversionError<T>(to: T.Type, sqliteStatement: SQLiteStatement, index: Int32, file: StaticString = #file, line: UInt = #line) -> Never {
let row = Row(sqliteStatement: sqliteStatement)
fatalConversionError(
to: T.self,
@ -146,11 +147,29 @@ func fatalConversionError<T>(to: T.Type, sqliteStatement: SQLiteStatement, index
conversionContext: ValueConversionContext(row).atColumn(Int(index)))
}
@usableFromInline
func fatalConversionError<T>(to: T.Type, from dbValue: DatabaseValue?, sqliteStatement: SQLiteStatement, index: Int32, file: StaticString = #file, line: UInt = #line) -> Never {
let row = Row(sqliteStatement: sqliteStatement)
fatalConversionError(
to: T.self,
from: dbValue,
conversionContext: ValueConversionContext(row).atColumn(Int(index)))
}
// MARK: - DatabaseValueConvertible
/// Lossless conversions from database values and rows
extension DatabaseValueConvertible {
@inline(__always)
@inlinable
static func decode(from sqliteStatement: SQLiteStatement, atUncheckedIndex index: Int32) -> Self {
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
if let value = fromDatabaseValue(dbValue) {
return value
} else {
fatalConversionError(to: Self.self, from: dbValue, sqliteStatement: sqliteStatement, index: index)
}
}
static func decode(from dbValue: DatabaseValue, conversionContext: @autoclosure () -> ValueConversionContext?) -> Self {
if let value = fromDatabaseValue(dbValue) {
return value
@ -159,14 +178,25 @@ extension DatabaseValueConvertible {
}
}
@inline(__always)
@usableFromInline
static func decode(from row: Row, atUncheckedIndex index: Int) -> Self {
return decode(
from: row.impl.databaseValue(atUncheckedIndex: index),
conversionContext: ValueConversionContext(row).atColumn(index))
}
@inline(__always)
@inlinable
static func decodeIfPresent(from sqliteStatement: SQLiteStatement, atUncheckedIndex index: Int32) -> Self? {
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
if let value = fromDatabaseValue(dbValue) {
return value
} else if dbValue.isNull {
return nil
} else {
fatalConversionError(to: Self.self, from: dbValue, sqliteStatement: sqliteStatement, index: index)
}
}
static func decodeIfPresent(from dbValue: DatabaseValue, conversionContext: @autoclosure () -> ValueConversionContext?) -> Self? {
// Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null.
if let value = fromDatabaseValue(dbValue) {
@ -178,7 +208,7 @@ extension DatabaseValueConvertible {
}
}
@inline(__always)
@usableFromInline
static func decodeIfPresent(from row: Row, atUncheckedIndex index: Int) -> Self? {
return decodeIfPresent(
from: row.impl.databaseValue(atUncheckedIndex: index),
@ -190,26 +220,23 @@ extension DatabaseValueConvertible {
/// Lossless conversions from database values and rows
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
@inline(__always)
static func fastDecode(from sqliteStatement: SQLiteStatement, index: Int32) -> Self {
@inlinable
static func fastDecode(from sqliteStatement: SQLiteStatement, atUncheckedIndex index: Int32) -> Self {
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
fatalConversionError(
to: Self.self,
from: .null,
conversionContext: ValueConversionContext(Row(sqliteStatement: sqliteStatement)).atColumn(Int(index)))
fatalConversionError(to: Self.self, sqliteStatement: sqliteStatement, index: index)
}
return self.init(sqliteStatement: sqliteStatement, index: index)
}
@inline(__always)
@inlinable
static func fastDecode(from row: Row, atUncheckedIndex index: Int) -> Self {
if let sqliteStatement = row.sqliteStatement {
return fastDecode(from: sqliteStatement, index: Int32(index))
return fastDecode(from: sqliteStatement, atUncheckedIndex: Int32(index))
}
return row.impl.fastDecode(Self.self, atUncheckedIndex: index)
return row.fastDecode(Self.self, atUncheckedIndex: index)
}
@inline(__always)
@inlinable
static func fastDecodeIfPresent(from sqliteStatement: SQLiteStatement, atUncheckedIndex index: Int32) -> Self? {
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
return nil
@ -217,11 +244,24 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
return self.init(sqliteStatement: sqliteStatement, index: index)
}
@inline(__always)
@inlinable
static func fastDecodeIfPresent(from row: Row, atUncheckedIndex index: Int) -> Self? {
if let sqliteStatement = row.sqliteStatement {
return fastDecodeIfPresent(from: sqliteStatement, atUncheckedIndex: Int32(index))
}
return row.impl.fastDecodeIfPresent(Self.self, atUncheckedIndex: index)
return row.fastDecodeIfPresent(Self.self, atUncheckedIndex: index)
}
}
// Support for @inlinable decoding
extension Row {
@usableFromInline
func fastDecode<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ type: Value.Type, atUncheckedIndex index: Int) -> Value {
return impl.fastDecode(type, atUncheckedIndex: index)
}
@usableFromInline
func fastDecodeIfPresent<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ type: Value.Type, atUncheckedIndex index: Int) -> Value? {
return impl.fastDecodeIfPresent(type, atUncheckedIndex: index)
}
}

View File

@ -12,11 +12,11 @@
/// The protocol comes with built-in methods that allow to fetch cursors,
/// arrays, or single values:
///
/// try String.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String
/// try String.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String]
/// try String.fetchOne(db, "SELECT name FROM ...", arguments:...) // String?
/// try String.fetchCursor(db, sql: "SELECT name FROM ...", arguments:...) // Cursor of String
/// try String.fetchAll(db, sql: "SELECT name FROM ...", arguments:...) // [String]
/// try String.fetchOne(db, sql: "SELECT name FROM ...", arguments:...) // String?
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// try String.fetchCursor(statement, arguments:...) // Cursor of String
/// try String.fetchAll(statement, arguments:...) // [String]
/// try String.fetchOne(statement, arguments:...) // String?
@ -42,58 +42,52 @@ extension DatabaseValueConvertible {
/// For example:
///
/// try dbQueue.read { db in
/// let urls: DatabaseValueCursor<URL> = try URL.fetchCursor(db, "SELECT url FROM link")
/// let urls: DatabaseValueCursor<URL> = try URL.fetchCursor(db, sql: "SELECT url FROM link")
/// while let url = urls.next() { // URL
/// print(url)
/// }
/// }
public final class DatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor {
private let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private let columnIndex: Int32
private var done = false
@usableFromInline let _statement: SelectStatement
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline let _columnIndex: Int32
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.sqliteStatement = statement.sqliteStatement
_statement = statement
_sqliteStatement = statement.sqliteStatement
if let adapter = adapter {
// adapter may redefine the index of the leftmost column
self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
_columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
self.columnIndex = 0
_columnIndex = 0
}
statement.reset(withArguments: arguments)
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Value? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex)
return Value.decode(
from: dbValue,
conversionContext: ValueConversionContext(statement).atColumn(Int(columnIndex)))
return Value.decode(from: _sqliteStatement, atUncheckedIndex: _columnIndex)
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(
resultCode: code,
message: statement.database.lastErrorMessage,
sql: statement.sql,
arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}
@ -102,58 +96,52 @@ public final class DatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor
/// For example:
///
/// try dbQueue.read { db in
/// let urls: NullableDatabaseValueCursor<URL> = try Optional<URL>.fetchCursor(db, "SELECT url FROM link")
/// let urls: NullableDatabaseValueCursor<URL> = try Optional<URL>.fetchCursor(db, sql: "SELECT url FROM link")
/// while let url = urls.next() { // URL?
/// print(url)
/// }
/// }
public final class NullableDatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor {
private let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private let columnIndex: Int32
private var done = false
@usableFromInline let _statement: SelectStatement
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline let _columnIndex: Int32
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.sqliteStatement = statement.sqliteStatement
_statement = statement
_sqliteStatement = statement.sqliteStatement
if let adapter = adapter {
// adapter may redefine the index of the leftmost column
self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
_columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
self.columnIndex = 0
_columnIndex = 0
}
statement.reset(withArguments: arguments)
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Value?? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex)
return Value.decodeIfPresent(
from: dbValue,
conversionContext: ValueConversionContext(statement).atColumn(Int(columnIndex)))
return Value.decodeIfPresent(from: _sqliteStatement, atUncheckedIndex: _columnIndex)
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(
resultCode: code,
message: statement.database.lastErrorMessage,
sql: statement.sql,
arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}
@ -161,11 +149,11 @@ public final class NullableDatabaseValueCursor<Value: DatabaseValueConvertible>
/// DatabaseValueConvertible comes with built-in methods that allow to fetch
/// cursors, arrays, or single values:
///
/// try String.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String
/// try String.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String]
/// try String.fetchOne(db, "SELECT name FROM ...", arguments:...) // String?
/// try String.fetchCursor(db, sql: "SELECT name FROM ...", arguments:...) // Cursor of String
/// try String.fetchAll(db, sql: "SELECT name FROM ...", arguments:...) // [String]
/// try String.fetchOne(db, sql: "SELECT name FROM ...", arguments:...) // String?
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// try String.fetchCursor(statement, arguments:...) // Cursor of String
/// try String.fetchAll(statement, arguments:...) // [String]
/// try String.fetchOne(statement, arguments:...) // String
@ -177,7 +165,7 @@ extension DatabaseValueConvertible {
/// Returns a cursor over values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try String.fetchCursor(statement) // Cursor of String
/// while let name = try names.next() { // String
/// ...
@ -194,13 +182,14 @@ extension DatabaseValueConvertible {
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> {
return try DatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try String.fetchAll(statement) // [String]
///
/// - parameters:
@ -209,6 +198,7 @@ extension DatabaseValueConvertible {
/// - adapter: Optional RowAdapter
/// - returns: An array.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
@ -218,7 +208,7 @@ extension DatabaseValueConvertible {
/// The result is nil if the query returns no row, or if no value can be
/// extracted from the first row.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let name = try String.fetchOne(statement) // String?
///
/// - parameters:
@ -227,6 +217,7 @@ extension DatabaseValueConvertible {
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
// fetchOne returns nil if there is no row, or if there is a row with a null value
let cursor = try NullableDatabaseValueCursor<Self>(statement: statement, arguments: arguments, adapter: adapter)
@ -240,7 +231,7 @@ extension DatabaseValueConvertible {
/// Returns a cursor over values fetched from an SQL query.
///
/// let names = try String.fetchCursor(db, "SELECT name FROM ...") // Cursor of String
/// let names = try String.fetchCursor(db, sql: "SELECT name FROM ...") // Cursor of String
/// while let name = try name.next() { // String
/// ...
/// }
@ -253,27 +244,29 @@ extension DatabaseValueConvertible {
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of values fetched from an SQL query.
///
/// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String]
/// let names = try String.fetchAll(db, sql: "SELECT name FROM ...") // [String]
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from an SQL query.
@ -281,17 +274,18 @@ extension DatabaseValueConvertible {
/// The result is nil if the query returns no row, or if no value can be
/// extracted from the first row.
///
/// let name = try String.fetchOne(db, "SELECT name FROM ...") // String?
/// let name = try String.fetchOne(db, sql: "SELECT name FROM ...") // String?
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchOne(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -317,6 +311,7 @@ extension DatabaseValueConvertible {
/// - request: A FetchRequest.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> DatabaseValueCursor<Self> {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -332,6 +327,7 @@ extension DatabaseValueConvertible {
/// - request: A FetchRequest.
/// - returns: An array.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Self] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -350,6 +346,7 @@ extension DatabaseValueConvertible {
/// - request: A FetchRequest.
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
return try fetchOne(statement, adapter: adapter)
@ -376,6 +373,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible {
/// - parameter db: A database connection.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> DatabaseValueCursor<RowDecoder> {
return try RowDecoder.fetchCursor(db, self)
}
@ -388,6 +386,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible {
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
return try RowDecoder.fetchAll(db, self)
}
@ -403,6 +402,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible {
/// - parameter db: A database connection.
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchOne(_ db: Database) throws -> RowDecoder? {
return try RowDecoder.fetchOne(db, self)
}
@ -411,10 +411,10 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible {
/// Swift's Optional comes with built-in methods that allow to fetch cursors
/// and arrays of optional DatabaseValueConvertible:
///
/// try Optional<String>.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String?]
/// try Optional<String>.fetchCursor(db, sql: "SELECT name FROM ...", arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(db, sql: "SELECT name FROM ...", arguments:...) // [String?]
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// try Optional<String>.fetchCursor(statement, arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(statement, arguments:...) // [String?]
///
@ -425,7 +425,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// Returns a cursor over optional values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try Optional<String>.fetchCursor(statement) // Cursor of String?
/// while let name = try names.next() { // String?
/// ...
@ -442,13 +442,14 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> {
return try NullableDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of optional values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try Optional<String>.fetchAll(statement) // [String?]
///
/// - parameters:
@ -457,6 +458,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - adapter: Optional RowAdapter
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
@ -468,7 +470,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// Returns a cursor over optional values fetched from an SQL query.
///
/// let names = try Optional<String>.fetchCursor(db, "SELECT name FROM ...") // Cursor of String?
/// let names = try Optional<String>.fetchCursor(db, sql: "SELECT name FROM ...") // Cursor of String?
/// while let name = try names.next() { // String?
/// ...
/// }
@ -481,27 +483,29 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of optional values fetched from an SQL query.
///
/// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String?]
/// let names = try String.fetchAll(db, sql: "SELECT name FROM ...") // [String?]
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - parameter arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -527,6 +531,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - request: A FetchRequest.
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> NullableDatabaseValueCursor<Wrapped> {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -542,6 +547,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - request: A FetchRequest.
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Wrapped?] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -568,6 +574,7 @@ extension FetchRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped:
/// - parameter db: A database connection.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> NullableDatabaseValueCursor<RowDecoder._Wrapped> {
return try Optional<RowDecoder._Wrapped>.fetchCursor(db, self)
}
@ -580,6 +587,7 @@ extension FetchRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped:
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [RowDecoder._Wrapped?] {
return try Optional<RowDecoder._Wrapped>.fetchAll(db, self)
}

View File

@ -62,31 +62,6 @@ public protocol DatabaseWriter : DatabaseReader {
// MARK: - Reading from Database
/// This method is deprecated. Use concurrentRead instead.
///
/// Synchronously or asynchronously executes a read-only block that takes a
/// database connection.
///
/// This method must be called from a writing dispatch queue, outside of any
/// transaction. You'll get a fatal error otherwise.
///
/// The *block* argument is guaranteed to see the database in the last
/// committed state at the moment this method is called. Eventual concurrent
/// database updates are *not visible* inside the block.
///
/// For example:
///
/// try writer.writeWithoutTransaction { db in
/// try db.execute("DELETE FROM player")
/// try writer.readFromCurrentState { db in
/// // Guaranteed to be zero
/// try Int.fetchOne(db, "SELECT COUNT(*) FROM player")!
/// }
/// try db.execute("INSERT INTO player ...")
/// }
@available(*, deprecated, message: "Use concurrentRead instead")
func readFromCurrentState(_ block: @escaping (Database) -> Void) throws
/// Concurrently executes a read-only block that takes a
/// database connection.
///
@ -119,7 +94,7 @@ public protocol DatabaseWriter : DatabaseReader {
/// // Guaranteed to be zero
/// let count = try future.wait()
/// }
func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> Future<T>
func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> DatabaseFuture<T>
}
extension DatabaseWriter {
@ -157,30 +132,30 @@ extension DatabaseWriter {
// So we'll drop all database objects one after the other.
try writeWithoutTransaction { db in
// Prevent foreign keys from messing with drop table statements
let foreignKeysEnabled = try Bool.fetchOne(db, "PRAGMA foreign_keys")!
let foreignKeysEnabled = try Bool.fetchOne(db, sql: "PRAGMA foreign_keys")!
if foreignKeysEnabled {
try db.execute("PRAGMA foreign_keys = OFF")
try db.execute(sql: "PRAGMA foreign_keys = OFF")
}
// Remove all database objects, one after the other
do {
try db.inTransaction {
while let row = try Row.fetchOne(db, "SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'") {
while let row = try Row.fetchOne(db, sql: "SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'") {
let type: String = row["type"]
let name: String = row["name"]
try db.execute("DROP \(type) \(name.quotedDatabaseIdentifier)")
try db.execute(sql: "DROP \(type) \(name.quotedDatabaseIdentifier)")
}
return .commit
}
// Restore foreign keys if needed
if foreignKeysEnabled {
try db.execute("PRAGMA foreign_keys = ON")
try db.execute(sql: "PRAGMA foreign_keys = ON")
}
} catch {
// Restore foreign keys if needed
if foreignKeysEnabled {
try? db.execute("PRAGMA foreign_keys = ON")
try? db.execute(sql: "PRAGMA foreign_keys = ON")
}
throw error
}
@ -197,7 +172,7 @@ extension DatabaseWriter {
///
/// See https://www.sqlite.org/lang_vacuum.html for more information.
public func vacuum() throws {
try writeWithoutTransaction { try $0.execute("VACUUM") }
try writeWithoutTransaction { try $0.execute(sql: "VACUUM") }
}
// MARK: - Value Observation
@ -235,7 +210,7 @@ extension DatabaseWriter {
DispatchQueue.main.async { onChange(value) }
}
}
case let .onQueue(queue, startImmediately: startImmediately):
case let .async(onQueue: queue, startImmediately: startImmediately):
if startImmediately {
if let value = try reducer.initialValue(db, requiresWriteAccess: observation.requiresWriteAccess) {
queue.async { onChange(value) }
@ -256,7 +231,7 @@ extension DatabaseWriter {
notificationQueue: observation.notificationQueue,
onError: onError,
onChange: onChange)
db.add(transactionObserver: valueObserver, extent: observation.extent)
db.add(transactionObserver: valueObserver, extent: .observerLifetime)
return valueObserver
}
@ -281,13 +256,13 @@ extension ValueReducer {
extension ValueObservation where Reducer: ValueReducer {
/// Helper method for DatabaseWriter.add(observation:onError:onChange:)
fileprivate func fetchAfterChange(in writer: DatabaseWriter) -> (Database, Reducer) -> Future<Reducer.Fetched> {
fileprivate func fetchAfterChange(in writer: DatabaseWriter) -> (Database, Reducer) -> DatabaseFuture<Reducer.Fetched> {
// The technique to return a future value after database has changed
// depends on the requiresWriteAccess flag:
if requiresWriteAccess {
// Synchronous fetch
return { (db, reducer) in
Future(Result {
DatabaseFuture(Result {
var fetchedValue: Reducer.Fetched!
try db.inTransaction {
fetchedValue = try reducer.fetch(db)
@ -305,8 +280,20 @@ extension ValueObservation where Reducer: ValueReducer {
}
}
/// A future value.
public class Future<Value> {
/// A future database value, returned by the DatabaseWriter.concurrentRead(_:)
/// method.
///
/// let futureCount: Future<Int> = try writer.writeWithoutTransaction { db in
/// try Player(...).insert()
///
/// // Count players concurrently
/// return writer.concurrentRead { db in
/// return try Player.fetchCount()
/// }
/// }
///
/// let count: Int = try futureCount.wait()
public class DatabaseFuture<Value> {
private var consumed = false
private let _wait: () throws -> Value
@ -315,7 +302,7 @@ public class Future<Value> {
}
init(_ result: Result<Value>) {
_wait = { try result.unwrap() }
_wait = result.get
}
/// Blocks the current thread until the value is available, and returns it.
@ -326,7 +313,7 @@ public class Future<Value> {
public func wait() throws -> Value {
// Not thread-safe and quick and dirty.
// Goal is that users learn not to call this method twice.
GRDBPrecondition(consumed == false, "Future.wait() must be called only once")
GRDBPrecondition(consumed == false, "DatabaseFuture.wait() must be called only once")
consumed = true
return try _wait()
}
@ -345,35 +332,29 @@ public final class AnyDatabaseWriter : DatabaseWriter {
}
// MARK: - Reading from Database
/// :nodoc:
public func read<T>(_ block: (Database) throws -> T) throws -> T {
return try base.read(block)
}
/// :nodoc:
public func unsafeRead<T>(_ block: (Database) throws -> T) throws -> T {
return try base.unsafeRead(block)
}
/// :nodoc:
public func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T {
return try base.unsafeReentrantRead(block)
}
/// :nodoc:
@available(*, deprecated, message: "Use concurrentRead instead")
public func readFromCurrentState(_ block: @escaping (Database) -> Void) throws {
try base.readFromCurrentState(block)
}
/// :nodoc:
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> Future<T> {
public func concurrentRead<T>(_ block: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
return base.concurrentRead(block)
}
// MARK: - Writing in Database
/// :nodoc:
public func write<T>(_ block: (Database) throws -> T) throws -> T {
return try base.write(block)
@ -383,7 +364,7 @@ public final class AnyDatabaseWriter : DatabaseWriter {
public func writeWithoutTransaction<T>(_ block: (Database) throws -> T) rethrows -> T {
return try base.writeWithoutTransaction(block)
}
/// :nodoc:
public func unsafeReentrantWrite<T>(_ block: (Database) throws -> T) rethrows -> T {
return try base.unsafeReentrantWrite(block)

View File

@ -47,7 +47,7 @@ extension FetchRequest {
public func fetchCount(_ db: Database) throws -> Int {
let (statement, _) = try prepare(db)
let sql = "SELECT COUNT(*) FROM (\(statement.sql))"
return try Int.fetchOne(db, sql, arguments: statement.arguments)!
return try Int.fetchOne(db, sql: sql, arguments: statement.arguments)!
}
/// Returns the database region that the request looks into.
@ -129,7 +129,7 @@ public struct AnyFetchRequest<T> : FetchRequest {
_fetchCount = { db in
let (statement, _) = try prepare(db)
let sql = "SELECT COUNT(*) FROM (\(statement.sql))"
return try Int.fetchOne(db, sql, arguments: statement.arguments)!
return try Int.fetchOne(db, sql: sql, arguments: statement.arguments)!
}
_databaseRegion = { db in
@ -153,98 +153,3 @@ public struct AnyFetchRequest<T> : FetchRequest {
return try _databaseRegion(db)
}
}
// MARK: - SQLRequest
/// A FetchRequest built from raw SQL.
public struct SQLRequest<T> : FetchRequest {
public typealias RowDecoder = T
public var sql: String
public var arguments: StatementArguments?
public var adapter: RowAdapter?
private let cache: Cache?
/// Creates a request from an SQL string, optional arguments, and
/// optional row adapter.
///
/// let request = SQLRequest("SELECT * FROM player")
/// let request = SQLRequest("SELECT * FROM player WHERE id = ?", arguments: [1])
///
/// - parameters:
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
/// - returns: A SQLRequest
public init(_ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil, cached: Bool = false) {
self.init(sql, arguments: arguments, adapter: adapter, fromCache: cached ? .public : nil)
}
/// Creates an SQL request from any other fetch request.
///
/// - parameters:
/// - db: A database connection.
/// - request: A request.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
/// - returns: An SQLRequest
public init<Request: FetchRequest>(_ db: Database, request: Request, cached: Bool = false) throws where Request.RowDecoder == RowDecoder {
let (statement, adapter) = try request.prepare(db)
self.init(statement.sql, arguments: statement.arguments, adapter: adapter, cached: cached)
}
/// Creates an SQL request from an SQL string, optional arguments, and
/// optional row adapter.
///
/// let request = SQLRequest("SELECT * FROM player")
/// let request = SQLRequest("SELECT * FROM player WHERE id = ?", arguments: [1])
///
/// - parameters:
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter.
/// - statementCacheName: Optional statement cache name.
/// - returns: A SQLRequest
init(_ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil, fromCache cache: Cache?) {
self.sql = sql
self.arguments = arguments
self.adapter = adapter
self.cache = cache
}
/// A tuple that contains a prepared statement that is ready to be
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
///
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
let statement: SelectStatement
switch cache {
case .none:
statement = try db.makeSelectStatement(sql)
case .public?:
statement = try db.cachedSelectStatement(sql)
case .internal?:
statement = try db.internalCachedSelectStatement(sql)
}
if let arguments = arguments {
try statement.setArgumentsWithValidation(arguments)
}
return (statement, adapter)
}
/// There are two statement caches: one for statements generated by the
/// user, and one for the statements generated by GRDB. Those are separated
/// so that GRDB has no opportunity to inadvertently modify the arguments of
/// user's cached statements.
enum Cache {
/// The public cache, for library user
case `public`
/// The internal cache, for grdb
case `internal`
}
}

View File

@ -14,15 +14,15 @@ public final class Row : Equatable, Hashable, RandomAccessCollection, Expressibl
/// Unless we are producing a row array, we use a single row when iterating
/// a statement:
///
/// let rows = try Row.fetchCursor(db, "SELECT ...")
/// let players = try Player.fetchAll(db, "SELECT ...")
/// let rows = try Row.fetchCursor(db, sql: "SELECT ...")
/// let players = try Player.fetchAll(db, sql: "SELECT ...")
///
/// This row keeps an unmanaged reference to the statement, and a handle to
/// the sqlite statement, so that we avoid many retain/release invocations.
///
/// The statementRef is released in deinit.
let statementRef: Unmanaged<SelectStatement>?
let sqliteStatement: SQLiteStatement?
@usableFromInline let sqliteStatement: SQLiteStatement?
var statement: SelectStatement? {
return statementRef?.takeUnretainedValue()
}
@ -86,6 +86,7 @@ public final class Row : Equatable, Hashable, RandomAccessCollection, Expressibl
/// The row is implemented on top of StatementRowImpl, which grants *direct*
/// access to the SQLite statement. Iteration of the statement does modify
/// the row.
@usableFromInline
init(statement: SelectStatement) {
let statementRef = Unmanaged.passRetained(statement) // released in deinit
self.statementRef = statementRef
@ -140,7 +141,7 @@ extension Row {
return index(ofColumn: columnName) != nil
}
@inline(__always)
@usableFromInline
func index(ofColumn name: String) -> Int? {
return impl.index(ofColumn: name)
}
@ -151,8 +152,8 @@ extension Row {
// MARK: - Extracting Values
/// Fatal errors if index is out of bounds
@inline(__always)
private func checkIndex(_ index: Int, file: StaticString = #file, line: UInt = #line) {
@inlinable
func _checkIndex(_ index: Int, file: StaticString = #file, line: UInt = #line) {
GRDBPrecondition(index >= 0 && index < count, "row index out of range", file: file, line: line)
}
@ -162,10 +163,10 @@ extension Row {
///
/// For example:
///
/// let row = try Row.fetchOne(db, "SELECT 'foo', 1")!
/// let row = try Row.fetchOne(db, sql: "SELECT 'foo', 1")!
/// row.containsNonNullValue // true
///
/// let row = try Row.fetchOne(db, "SELECT NULL, NULL")!
/// let row = try Row.fetchOne(db, sql: "SELECT NULL, NULL")!
/// row.containsNonNullValue // false
public var containsNonNullValue: Bool {
for i in (0..<count) where !impl.hasNull(atUncheckedIndex: i) {
@ -188,7 +189,7 @@ extension Row {
/// in performance-critical code because it can avoid decoding database
/// values.
public func hasNull(atIndex index: Int) -> Bool {
checkIndex(index)
_checkIndex(index)
return impl.hasNull(atUncheckedIndex: index)
}
@ -198,7 +199,7 @@ extension Row {
/// Indexes span from 0 for the leftmost column to (row.count - 1) for the
/// righmost column.
public subscript(_ index: Int) -> DatabaseValueConvertible? {
checkIndex(index)
_checkIndex(index)
return impl.databaseValue(atUncheckedIndex: index).storage.value
}
@ -210,8 +211,9 @@ extension Row {
/// If the SQLite value is NULL, the result is nil. Otherwise the SQLite
/// value is converted to the requested type `Value`. Should this conversion
/// fail, a fatal error is raised.
@inlinable
public subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value? {
checkIndex(index)
_checkIndex(index)
return Value.decodeIfPresent(from: self, atUncheckedIndex: index)
}
@ -227,8 +229,9 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ index: Int) -> Value? {
checkIndex(index)
_checkIndex(index)
return Value.fastDecodeIfPresent(from: self, atUncheckedIndex: index)
}
@ -239,8 +242,9 @@ extension Row {
///
/// This method crashes if the fetched SQLite value is NULL, or if the
/// SQLite value can not be converted to `Value`.
@inlinable
public subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value {
checkIndex(index)
_checkIndex(index)
return Value.decode(from: self, atUncheckedIndex: index)
}
@ -255,8 +259,9 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ index: Int) -> Value {
checkIndex(index)
_checkIndex(index)
return Value.fastDecode(from: self, atUncheckedIndex: index)
}
@ -289,6 +294,7 @@ extension Row {
/// If the column is missing or if the SQLite value is NULL, the result is
/// nil. Otherwise the SQLite value is converted to the requested type
/// `Value`. Should this conversion fail, a fatal error is raised.
@inlinable
public subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value? {
guard let index = index(ofColumn: columnName) else {
return nil
@ -308,6 +314,7 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ columnName: String) -> Value? {
guard let index = index(ofColumn: columnName) else {
return nil
@ -324,13 +331,11 @@ extension Row {
///
/// This method crashes if the fetched SQLite value is NULL, or if the
/// SQLite value can not be converted to `Value`.
@inlinable
public subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value {
guard let index = index(ofColumn: columnName) else {
// No such column
fatalConversionError(
to: Value.self,
from: nil,
conversionContext: ValueConversionContext(self).atColumn(columnName))
fatalConversionError(to: Value.self, from: nil, in: self, atColumn: columnName)
}
return Value.decode(from: self, atUncheckedIndex: index)
}
@ -348,13 +353,11 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ columnName: String) -> Value {
guard let index = index(ofColumn: columnName) else {
// No such column
fatalConversionError(
to: Value.self,
from: nil,
conversionContext: ValueConversionContext(self).atColumn(columnName))
fatalConversionError(to: Value.self, from: nil, in: self, atColumn: columnName)
}
return Value.fastDecode(from: self, atUncheckedIndex: index)
}
@ -366,7 +369,8 @@ extension Row {
/// the same name, the leftmost column is considered.
///
/// The result is nil if the row does not contain the column.
public subscript(_ column: ColumnExpression) -> DatabaseValueConvertible? {
@inlinable
public subscript<Column: ColumnExpression>(_ column: Column) -> DatabaseValueConvertible? {
return self[column.name]
}
@ -378,7 +382,8 @@ extension Row {
/// If the column is missing or if the SQLite value is NULL, the result is
/// nil. Otherwise the SQLite value is converted to the requested type
/// `Value`. Should this conversion fail, a fatal error is raised.
public subscript<Value: DatabaseValueConvertible>(_ column: ColumnExpression) -> Value? {
@inlinable
public subscript<Value: DatabaseValueConvertible, Column: ColumnExpression>(_ column: Column) -> Value? {
return self[column.name]
}
@ -394,7 +399,8 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ column: ColumnExpression) -> Value? {
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible, Column: ColumnExpression>(_ column: Column) -> Value? {
return self[column.name]
}
@ -407,7 +413,8 @@ extension Row {
///
/// This method crashes if the fetched SQLite value is NULL, or if the
/// SQLite value can not be converted to `Value`.
public subscript<Value: DatabaseValueConvertible>(_ column: ColumnExpression) -> Value {
@inlinable
public subscript<Value: DatabaseValueConvertible, Column: ColumnExpression>(_ column: Column) -> Value {
return self[column.name]
}
@ -424,7 +431,8 @@ extension Row {
/// This method exists as an optimization opportunity for types that adopt
/// StatementColumnConvertible. It *may* trigger SQLite built-in conversions
/// (see https://www.sqlite.org/datatype3.html).
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ column: ColumnExpression) -> Value {
@inlinable
public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible, Column: ColumnExpression>(_ column: Column) -> Value {
return self[column.name]
}
@ -439,7 +447,7 @@ extension Row {
/// The returned data does not owns its bytes: it must not be used longer
/// than the row's lifetime.
public func dataNoCopy(atIndex index: Int) -> Data? {
checkIndex(index)
_checkIndex(index)
return impl.dataNoCopy(atUncheckedIndex: index)
}
@ -472,7 +480,7 @@ extension Row {
///
/// The returned data does not owns its bytes: it must not be used longer
/// than the row's lifetime.
public func dataNoCopy(_ column: ColumnExpression) -> Data? {
public func dataNoCopy<Column: ColumnExpression>(_ column: Column) -> Data? {
return dataNoCopy(named: column.name)
}
}
@ -541,7 +549,7 @@ extension Row {
///
/// // Fetch
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
/// let row = try Row.fetchOne(db, sql, adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row.scopes.count // 2
/// row.scopes.names // ["foo", "bar"]
@ -565,7 +573,7 @@ extension Row {
///
/// // Fetch
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
/// let row = try Row.fetchOne(db, sql, adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row.scopesTree.names // ["foo", "bar", "baz"]
///
@ -602,18 +610,19 @@ extension Row {
/// A cursor of database rows. For example:
///
/// try dbQueue.read { db in
/// let rows: RowCursor = try Row.fetchCursor(db, "SELECT * FROM player")
/// let rows: RowCursor = try Row.fetchCursor(db, sql: "SELECT * FROM player")
/// }
public final class RowCursor : Cursor {
public let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private let row: Row // Reused for performance
private var done = false
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline let _row: Row // Reused for performance
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.row = try Row(statement: statement).adapted(with: adapter, layout: statement)
self.sqliteStatement = statement.sqliteStatement
self._row = try Row(statement: statement).adapted(with: adapter, layout: statement)
self._sqliteStatement = statement.sqliteStatement
statement.reset(withArguments: arguments)
}
@ -624,21 +633,21 @@ public final class RowCursor : Cursor {
}
/// :nodoc:
@inlinable
public func next() throws -> Row? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
return row
return _row
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments)
try statement.didFail(withResultCode: code)
}
}
}
@ -649,7 +658,7 @@ extension Row {
/// Returns a cursor over rows fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT ...")
/// let rows = try Row.fetchCursor(statement) // RowCursor
/// while let row = try rows.next() { // Row
/// let id: Int64 = row[0]
@ -676,13 +685,14 @@ extension Row {
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RowCursor {
return try RowCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of rows fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT ...")
/// let rows = try Row.fetchAll(statement)
///
/// - parameters:
@ -691,6 +701,7 @@ extension Row {
/// - adapter: Optional RowAdapter
/// - returns: An array of rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Row] {
// The cursor reuses a single mutable row. Return immutable copies.
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() })
@ -698,7 +709,7 @@ extension Row {
/// Returns a single row fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT ...")
/// let row = try Row.fetchOne(statement)
///
/// - parameters:
@ -707,6 +718,7 @@ extension Row {
/// - adapter: Optional RowAdapter
/// - returns: An optional row.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Row? {
let cursor = try fetchCursor(statement, arguments: arguments, adapter: adapter)
// Keep cursor alive until we can copy the fetched row
@ -722,7 +734,7 @@ extension Row {
/// Returns a cursor over rows fetched from an SQL query.
///
/// let rows = try Row.fetchCursor(db, "SELECT id, name FROM player") // RowCursor
/// let rows = try Row.fetchCursor(db, sql: "SELECT id, name FROM player") // RowCursor
/// while let row = try rows.next() { // Row
/// let id: Int64 = row[0]
/// let name: String = row[1]
@ -744,42 +756,53 @@ extension Row {
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RowCursor {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> RowCursor {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of rows fetched from an SQL query.
///
/// let rows = try Row.fetchAll(db, "SELECT ...")
/// let rows = try Row.fetchAll(db, sql: "SELECT id, name FROM player") // [Row]
/// for row in rows {
/// let id: Int64 = row[0]
/// let name: String = row[1]
/// }
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Row] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Row] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single row fetched from an SQL query.
///
/// let row = try Row.fetchOne(db, "SELECT ...")
/// let row = try Row.fetchOne(db, sql: "SELECT id, name FROM player") // Row?
/// if let row = row {
/// let id: Int64 = row[0]
/// let name: String = row[1]
/// }
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional row.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Row? {
return try fetchOne(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchOne(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> Row? {
return try fetchOne(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -814,6 +837,7 @@ extension Row {
/// - request: A FetchRequest.
/// - returns: A cursor over fetched rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> RowCursor {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -829,6 +853,7 @@ extension Row {
/// - request: A FetchRequest.
/// - returns: An array of rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Row] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -844,6 +869,7 @@ extension Row {
/// - request: A FetchRequest.
/// - returns: An optional row.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Row? {
let (statement, adapter) = try request.prepare(db)
return try fetchOne(statement, adapter: adapter)
@ -879,6 +905,7 @@ extension FetchRequest where RowDecoder: Row {
/// - parameter db: A database connection.
/// - returns: A cursor over fetched rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> RowCursor {
return try Row.fetchCursor(db, self)
}
@ -891,6 +918,7 @@ extension FetchRequest where RowDecoder: Row {
/// - parameter db: A database connection.
/// - returns: An array of fetched rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [Row] {
return try Row.fetchAll(db, self)
}
@ -903,6 +931,7 @@ extension FetchRequest where RowDecoder: Row {
/// - parameter db: A database connection.
/// - returns: A,n optional rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchOne(_ db: Database) throws -> Row? {
return try Row.fetchOne(db, self)
}
@ -943,7 +972,7 @@ extension Row {
/// Accesses the (ColumnName, DatabaseValue) pair at given index.
public subscript(position: RowIndex) -> (String, DatabaseValue) {
let index = position.index
checkIndex(index)
_checkIndex(index)
return (
impl.columnName(atUncheckedIndex: index),
impl.databaseValue(atUncheckedIndex: index))
@ -994,7 +1023,6 @@ extension Row {
// Hashable
extension Row {
#if swift(>=4.2)
/// :nodoc:
public func hash(into hasher: inout Hasher) {
hasher.combine(count)
@ -1003,13 +1031,6 @@ extension Row {
hasher.combine(dbValue)
}
}
#else
/// :nodoc:
public var hashValue: Int {
return columnNames.reduce(0) { (acc, column) in acc ^ column.hashValue } ^
databaseValues.reduce(0) { (acc, dbValue) in acc ^ dbValue.hashValue }
}
#endif
}
// CustomStringConvertible & CustomDebugStringConvertible
@ -1101,7 +1122,7 @@ extension Row {
///
/// // Fetch
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
/// let row = try Row.fetchOne(db, sql, adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row.scopes.count // 2
/// row.scopes.names // ["foo", "bar"]
@ -1176,7 +1197,7 @@ extension Row {
///
/// // Fetch
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
/// let row = try Row.fetchOne(db, sql, adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row.scopesTree.names // ["foo", "bar", "baz"]
///
@ -1317,7 +1338,7 @@ private struct ArrayRowImpl : RowImpl {
func index(ofColumn name: String) -> Int? {
let lowercaseName = name.lowercased()
return columns.index { (column, _) in column.lowercased() == lowercaseName }
return columns.firstIndex { (column, _) in column.lowercased() == lowercaseName }
}
func copiedRow(_ row: Row) -> Row {
@ -1356,7 +1377,7 @@ private struct StatementCopyRowImpl : RowImpl {
func index(ofColumn name: String) -> Int? {
let lowercaseName = name.lowercased()
return columnNames.index { $0.lowercased() == lowercaseName }
return columnNames.firstIndex { $0.lowercased() == lowercaseName }
}
func copiedRow(_ row: Row) -> Row {
@ -1415,7 +1436,7 @@ private struct StatementRowImpl : RowImpl {
_ type: Value.Type,
atUncheckedIndex index: Int) -> Value
{
return Value.fastDecode(from: sqliteStatement, index: Int32(index))
return Value.fastDecode(from: sqliteStatement, atUncheckedIndex: Int32(index))
}
func fastDecodeIfPresent<Value: DatabaseValueConvertible & StatementColumnConvertible>(

View File

@ -13,7 +13,7 @@ import Foundation
/// "b": adapters[1],
/// "c": adapters[2],
/// "d": adapters[3]])
/// let row = try Row.fetchOne(db, sql, adapter: adapter)
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)
/// row.scopes["a"] // [1]
/// row.scopes["b"] // [2, 3, 4]
/// row.scopes["c"] // [5, 6]
@ -78,7 +78,7 @@ public struct LayoutedColumnMapping {
/// }
///
/// // [foo:"foo" bar: "bar"]
/// try Row.fetchOne(db, "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter())
/// try Row.fetchOne(db, sql: "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter())
public init<S: Sequence>(layoutColumns: S) where S.Iterator.Element == (Int, String) {
self.layoutColumns = Array(layoutColumns)
self.lowercaseColumnIndexes = Dictionary(
@ -201,7 +201,7 @@ extension SelectStatement : RowLayout {
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
///
/// // [baz:3]
/// try Row.fetchOne(db, sql, adapter: adapter)
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
public protocol RowAdapter {
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
@ -224,7 +224,7 @@ public protocol RowAdapter {
/// }
///
/// // [foo:1]
/// try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: FirstColumnAdapter())
/// try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: FirstColumnAdapter())
func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter
}
@ -246,6 +246,7 @@ extension RowAdapter {
}
extension RowAdapter {
@usableFromInline
func baseColumnIndex(atIndex index: Int, layout: RowLayout) throws -> Int {
return try layoutedAdapter(from: layout).mapping.baseColumnIndex(atMappingIndex: index)
}
@ -269,7 +270,7 @@ public struct EmptyRowAdapter: RowAdapter {
/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar, 'baz' AS baz"
///
/// // [foo:"bar"]
/// try Row.fetchOne(db, sql, adapter: adapter)
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
public struct ColumnMapping : RowAdapter {
/// A dictionary from mapped column names to column names in a base row.
let mapping: [String: String]
@ -303,7 +304,7 @@ public struct ColumnMapping : RowAdapter {
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
///
/// // [baz:3]
/// try Row.fetchOne(db, sql, adapter: adapter)
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
public struct SuffixRowAdapter : RowAdapter {
/// The suffix index
let index: Int
@ -330,7 +331,7 @@ public struct SuffixRowAdapter : RowAdapter {
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz, 4 as qux"
///
/// // [bar:2 baz:3]
/// try Row.fetchOne(db, sql, adapter: adapter)
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
public struct RangeRowAdapter : RowAdapter {
/// The range
let range: CountableRange<Int>
@ -367,7 +368,7 @@ public struct RangeRowAdapter : RowAdapter {
///
/// // Fetch
/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar"
/// let row = try Row.fetchOne(db, sql, adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// // Scoped rows:
/// if let fooRow = row.scopes["foo"] {
@ -389,7 +390,7 @@ public struct ScopeAdapter : RowAdapter {
/// For example:
///
/// let adapter = ScopeAdapter(["suffix": SuffixRowAdapter(fromIndex: 1)])
/// let row = try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
/// row // [1, 2, 3]
/// row.scopes["suffix"] // [2, 3]
///
@ -406,7 +407,7 @@ public struct ScopeAdapter : RowAdapter {
///
/// let baseAdapter = RangeRowAdapter(0..<1)
/// let adapter = ScopeAdapter(base: baseAdapter, scopes: ["suffix": SuffixRowAdapter(fromIndex: 1)])
/// let row = try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: adapter)!
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
/// row // [1]
/// row.scopes["initial"] // [2, 3]
///
@ -460,6 +461,7 @@ extension Row {
}
/// Returns self if adapter is nil
@usableFromInline
func adapted(with adapter: RowAdapter?, layout: RowLayout) throws -> Row {
guard let adapter = adapter else {
return self

View File

@ -0,0 +1,33 @@
#if swift(>=5.0)
/// :nodoc:
public struct SQLInterpolation: StringInterpolationProtocol {
var context = SQLGenerationContext.literalGenerationContext(withArguments: true)
var sql: String
var arguments: StatementArguments {
get { return context.arguments! }
set { context.arguments = newValue }
}
public init(literalCapacity: Int, interpolationCount: Int) {
sql = ""
sql.reserveCapacity(literalCapacity + interpolationCount)
}
/// "SELECT * FROM player"
public mutating func appendLiteral(_ sql: String) {
self.sql += sql
}
/// "SELECT * FROM \(sql: "player")"
public mutating func appendInterpolation(sql: String, arguments: StatementArguments = StatementArguments()) {
self.sql += sql
self.arguments += arguments
}
/// "SELECT * FROM player WHERE \(literal: condition)"
public mutating func appendInterpolation(literal sqlLiteral: SQLLiteral) {
sql += sqlLiteral.sql
arguments += sqlLiteral.arguments
}
}
#endif

145
GRDB/Core/SQLLiteral.swift Normal file
View File

@ -0,0 +1,145 @@
/// SQLLiteral is a type which support [SQL Interpolation](https://github.com/groue/GRDB.swift/blob/master/Documentation/SQLInterpolation.md).
///
/// For example:
///
/// try dbQueue.write { db in
/// let name: String = ...
/// let id: Int64 = ...
/// let query: SQLLiteral = "UPDATE player SET name = \(name) WHERE id = \(id)"
/// try db.execute(literal: query)
/// }
public struct SQLLiteral {
private(set) public var sql: String
private(set) public var arguments: StatementArguments
/// Creates an SQLLiteral from a plain SQL string, and eventual arguments.
///
/// For example:
///
/// let query = SQLLiteral(
/// sql: "UPDATE player SET name = ? WHERE id = ?",
/// arguments: [name, id])
public init(sql: String, arguments: StatementArguments = StatementArguments()) {
self.sql = sql
self.arguments = arguments
}
/// Returns a literal whose SQL is transformed by the given closure.
public func mapSQL(_ transform: (String) throws -> String) rethrows -> SQLLiteral {
var result = self
result.sql = try transform(sql)
return result
}
}
extension SQLLiteral {
/// Returns the SQLLiteral produced by the concatenation of two literals.
///
/// let name = "O'Brien"
/// let selection: SQLLiteral = "SELECT * FROM player "
/// let condition: SQLLiteral = "WHERE name = \(name)"
/// let query = selection + condition
public static func + (lhs: SQLLiteral, rhs: SQLLiteral) -> SQLLiteral {
var result = lhs
result += rhs
return result
}
/// Appends an SQLLiteral to the receiver.
///
/// let name = "O'Brien"
/// var query: SQLLiteral = "SELECT * FROM player "
/// query += "WHERE name = \(name)"
public static func += (lhs: inout SQLLiteral, rhs: SQLLiteral) {
lhs.sql += rhs.sql
lhs.arguments += rhs.arguments
}
/// Appends an SQLLiteral to the receiver.
///
/// let name = "O'Brien"
/// var query: SQLLiteral = "SELECT * FROM player "
/// query.append(literal: "WHERE name = \(name)")
public mutating func append(literal sqlLiteral: SQLLiteral) {
self += sqlLiteral
}
/// Appends a plain SQL string to the receiver, and eventual arguments.
///
/// let name = "O'Brien"
/// var query: SQLLiteral = "SELECT * FROM player "
/// query.append(sql: "WHERE name = ?", arguments: [name])
public mutating func append(sql: String, arguments: StatementArguments = StatementArguments()) {
self += SQLLiteral(sql: sql, arguments: arguments)
}
}
extension Sequence where Element == SQLLiteral {
/// Returns the concatenated SQLLiteral of this sequence of literals,
/// inserting the given separator between each element.
///
/// let components: [SQLLiteral] = [
/// "UPDATE player",
/// "SET name = \(name)",
/// "WHERE id = \(id)"
/// ]
/// let query = components.joined(separator: " ")
public func joined(separator: String = "") -> SQLLiteral {
var sql = ""
var arguments = StatementArguments()
var first = true
for literal in self {
if first {
first = false
} else {
sql += separator
}
sql += literal.sql
arguments += literal.arguments
}
return SQLLiteral(sql: sql, arguments: arguments)
}
}
extension Collection where Element == SQLLiteral {
/// Returns the concatenated SQLLiteral of this collection of literals,
/// inserting the given separator between each element.
///
/// let components: [SQLLiteral] = [
/// "UPDATE player",
/// "SET name = \(name)",
/// "WHERE id = \(id)"
/// ]
/// let query = components.joined(separator: " ")
public func joined(separator: String = "") -> SQLLiteral {
let sql = map { $0.sql }.joined(separator: separator)
let arguments = reduce(into: StatementArguments()) { $0 += $1.arguments }
return SQLLiteral(sql: sql, arguments: arguments)
}
}
// MARK: - ExpressibleByStringInterpolation
#if swift(>=5.0)
extension SQLLiteral: ExpressibleByStringInterpolation {
/// :nodoc
public init(unicodeScalarLiteral: String) {
self.init(sql: unicodeScalarLiteral, arguments: [])
}
/// :nodoc:
public init(extendedGraphemeClusterLiteral: String) {
self.init(sql: extendedGraphemeClusterLiteral, arguments: [])
}
/// :nodoc:
public init(stringLiteral: String) {
self.init(sql: stringLiteral, arguments: [])
}
/// :nodoc:
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
self.init(sql: sqlInterpolation.sql, arguments: sqlInterpolation.arguments)
}
}
#endif

167
GRDB/Core/SQLRequest.swift Normal file
View File

@ -0,0 +1,167 @@
/// A FetchRequest built from raw SQL.
public struct SQLRequest<T> : FetchRequest {
/// There are two statement caches: one "public" for statements generated by
/// the user, and one "internal" for the statements generated by GRDB. Those
/// are separated so that GRDB has no opportunity to inadvertently modify
/// the arguments of user's cached statements.
enum Cache {
/// The public cache, for library user
case `public`
/// The internal cache, for GRDB
case `internal`
}
public typealias RowDecoder = T
/// The raw SQL query
///
/// let id = 42
/// let request: SQLRequest<Player> = "SELECT * FROM player WHERE id = \(id)"
/// request.sql // "SELECT * FROM player WHERE id = ?"
public var sql: String { return sqlLiteral.sql }
/// The request argument
///
/// let id = 42
/// let request: SQLRequest<Player> = "SELECT * FROM player WHERE id = \(id)"
/// request.arguments // [42]
public var arguments: StatementArguments { return sqlLiteral.arguments }
/// The request adapter
public var adapter: RowAdapter?
private var sqlLiteral: SQLLiteral
private let cache: Cache?
/// Creates a request from an SQL string, optional arguments, and
/// optional row adapter.
///
/// let request = SQLRequest<String>(sql: """
/// SELECT name FROM player
/// """)
/// let request = SQLRequest<Player>(sql: """
/// SELECT * FROM player WHERE id = ?
/// """, arguments: [1])
///
/// - parameters:
/// - sql: An SQL query.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
/// - returns: A SQLRequest
public init(sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil, cached: Bool = false) {
self.init(literal: SQLLiteral(sql: sql, arguments: arguments), adapter: adapter, fromCache: cached ? .public : nil)
}
/// Creates a request from an SQLLiteral, and optional row adapter.
///
/// let request = SQLRequest<String>(literal: SQLLiteral(sql: """
/// SELECT name FROM player
/// """))
/// let request = SQLRequest<Player>(literal: SQLLiteral(sql: """
/// SELECT * FROM player WHERE name = ?
/// """, arguments: ["O'Brien"]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// let request = SQLRequest<Player>(literal: """
/// SELECT * FROM player WHERE name = \("O'brien")
/// """)
///
/// - parameters:
/// - sqlLiteral: An SQLLiteral.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
/// - returns: A SQLRequest
public init(literal sqlLiteral: SQLLiteral, adapter: RowAdapter? = nil, cached: Bool = false) {
self.init(literal: sqlLiteral, adapter: adapter, fromCache: cached ? .public : nil)
}
/// Creates an SQL request from any other fetch request.
///
/// - parameters:
/// - db: A database connection.
/// - request: A request.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
/// - returns: An SQLRequest
public init<Request: FetchRequest>(_ db: Database, request: Request, cached: Bool = false) throws where Request.RowDecoder == RowDecoder {
let (statement, adapter) = try request.prepare(db)
self.init(literal: SQLLiteral(sql: statement.sql, arguments: statement.arguments), adapter: adapter, cached: cached)
}
/// Creates a request from an SQLLiteral, and optional row adapter.
///
/// let request = SQLRequest<String>(literal: SQLLiteral(sql: """
/// SELECT name FROM player
/// """))
/// let request = SQLRequest<Player>(literal: SQLLiteral(sql: """
/// SELECT * FROM player WHERE name = ?
/// """, arguments: ["O'Brien"]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// let request = SQLRequest<Player>(literal: """
/// SELECT * FROM player WHERE name = \("O'brien")
/// """)
///
/// - parameters:
/// - sqlLiteral: An SQLLiteral.
/// - adapter: Optional RowAdapter.
/// - cache: The eventual cache
/// - returns: A SQLRequest
init(literal sqlLiteral: SQLLiteral, adapter: RowAdapter? = nil, fromCache cache: Cache?) {
self.sqlLiteral = sqlLiteral
self.adapter = adapter
self.cache = cache
}
/// A tuple that contains a prepared statement that is ready to be
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
///
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
let statement: SelectStatement
switch cache {
case .none:
statement = try db.makeSelectStatement(sql: sqlLiteral.sql)
case .public?:
statement = try db.cachedSelectStatement(sql: sqlLiteral.sql)
case .internal?:
statement = try db.internalCachedSelectStatement(sql: sqlLiteral.sql)
}
try statement.setArgumentsWithValidation(sqlLiteral.arguments)
return (statement, adapter)
}
}
#if swift(>=5.0)
extension SQLRequest: ExpressibleByStringInterpolation {
/// :nodoc
public init(unicodeScalarLiteral: String) {
self.init(sql: unicodeScalarLiteral)
}
/// :nodoc:
public init(extendedGraphemeClusterLiteral: String) {
self.init(sql: extendedGraphemeClusterLiteral)
}
/// :nodoc:
public init(stringLiteral: String) {
self.init(sql: stringLiteral)
}
/// :nodoc:
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
self.init(literal: SQLLiteral(stringInterpolation: sqlInterpolation))
}
}
#endif

View File

@ -44,7 +44,7 @@ final class SchedulingWatchdog {
}
static func preconditionValidQueue(_ db: Database, _ message: @autoclosure() -> String = "Database was not used on the correct thread.", file: StaticString = #file, line: UInt = #line) {
GRDBPrecondition(current?.allows(db) ?? false, message, file: file, line: line)
GRDBPrecondition(current?.allows(db) ?? false, message(), file: file, line: line)
}
static var current: SchedulingWatchdog? {

View File

@ -189,11 +189,11 @@ final class SerializedDatabase {
/// Fatal error if current dispatch queue is not valid.
func preconditionValidQueue(_ message: @autoclosure() -> String = "Database was not used on the correct thread.", file: StaticString = #file, line: UInt = #line) {
SchedulingWatchdog.preconditionValidQueue(db, message, file: file, line: line)
SchedulingWatchdog.preconditionValidQueue(db, message(), file: file, line: line)
}
/// Fatal error if a transaction has been left opened.
private func preconditionNoUnsafeTransactionLeft(_ db: Database, _ message: @autoclosure() -> String = "A transaction has been left opened at the end of a database access", file: StaticString = #file, line: UInt = #line) {
GRDBPrecondition(configuration.allowsUnsafeTransactions || !db.isInsideTransaction, message, file: file, line: line)
GRDBPrecondition(configuration.allowsUnsafeTransactions || !db.isInsideTransaction, message(), file: file, line: line)
}
}

View File

@ -8,11 +8,9 @@ import Foundation
/// A raw SQLite statement, suitable for the SQLite C API.
public typealias SQLiteStatement = OpaquePointer
/// Statements are separated by semicolons and white spaces
let statementSeparatorCharacterSet = CharacterSet(charactersIn: ";").union(.whitespacesAndNewlines)
/// An error emitted when one tries to compile an empty statement.
struct EmptyStatementError : Error {
extension CharacterSet {
/// Statements are separated by semicolons and white spaces
static let sqlStatementSeparators = CharacterSet(charactersIn: ";").union(.whitespacesAndNewlines)
}
/// A statement represents an SQL query.
@ -28,12 +26,13 @@ public class Statement {
public var sql: String {
// trim white space and semicolumn for homogeneous output
return String(cString: sqlite3_sql(sqliteStatement))
.trimmingCharacters(in: statementSeparatorCharacterSet)
.trimmingCharacters(in: .sqlStatementSeparators)
}
unowned let database: Database
/// Creates a prepared statement.
/// Creates a prepared statement. Returns nil if the compiled string is
/// blank or empty.
///
/// - parameter database: A database connection.
/// - parameter statementStart: A pointer to a UTF-8 encoded C string
@ -42,23 +41,27 @@ public class Statement {
/// statement in the C string.
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
/// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html)
/// - throws: DatabaseError in case of compilation error, and
/// EmptyStatementError if the compiled string is blank or empty.
init(
/// - throws: DatabaseError in case of compilation error.
required init?(
database: Database,
statementStart: UnsafePointer<Int8>,
statementEnd: UnsafeMutablePointer<UnsafePointer<Int8>?>,
prepFlags: Int32) throws
prepFlags: Int32,
authorizer: StatementCompilationAuthorizer) throws
{
SchedulingWatchdog.preconditionValidQueue(database)
var sqliteStatement: SQLiteStatement? = nil
// sqlite3_prepare_v3 was introduced in SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
#if GRDBCUSTOMSQLITE || GRDBCIPHER
let code = sqlite3_prepare_v3(database.sqliteConnection, statementStart, -1, UInt32(bitPattern: prepFlags), &sqliteStatement, statementEnd)
let code = sqlite3_prepare_v3(database.sqliteConnection, statementStart, -1, UInt32(bitPattern: prepFlags), &sqliteStatement, statementEnd)
#else
// TODO: use sqlite3_prepare_v3 if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *)
let code = sqlite3_prepare_v2(database.sqliteConnection, statementStart, -1, &sqliteStatement, statementEnd)
let code: Int32
if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
code = sqlite3_prepare_v3(database.sqliteConnection, statementStart, -1, UInt32(bitPattern: prepFlags), &sqliteStatement, statementEnd)
} else {
code = sqlite3_prepare_v2(database.sqliteConnection, statementStart, -1, &sqliteStatement, statementEnd)
}
#endif
guard code == SQLITE_OK else {
@ -66,13 +69,7 @@ public class Statement {
}
guard let statement = sqliteStatement else {
// I wish we could simply return nil, and make this initializer failable.
//
// Unfortunately, there is a Swift bug with failable+throwing initializers:
// https://bugs.swift.org/browse/SR-6067
//
// We thus use sentinel error for empty statements.
throw EmptyStatementError()
return nil
}
self.database = database
@ -95,7 +92,7 @@ public class Statement {
// MARK: Arguments
var argumentsNeedValidation = true
var _arguments: StatementArguments = []
var _arguments = StatementArguments()
lazy var sqliteArgumentCount: Int = {
Int(sqlite3_bind_parameter_count(self.sqliteStatement))
@ -125,7 +122,7 @@ public class Statement {
/// statement arguments.
public func validate(arguments: StatementArguments) throws {
var arguments = arguments
_ = try arguments.consume(self, allowingRemainingValues: false)
_ = try arguments.extractBindings(forStatement: self, allowingRemainingValues: false)
}
/// Set arguments without any validation. Trades safety for performance.
@ -152,7 +149,7 @@ public class Statement {
// Validate
_arguments = arguments
var arguments = arguments
let bindings = try arguments.consume(self, allowingRemainingValues: false)
let bindings = try arguments.extractBindings(forStatement: self, allowingRemainingValues: false)
argumentsNeedValidation = false
// Apply
@ -176,9 +173,15 @@ public class Statement {
case .string(let string):
code = sqlite3_bind_text(sqliteStatement, index, string, -1, SQLITE_TRANSIENT)
case .blob(let data):
code = data.withUnsafeBytes { bytes in
sqlite3_bind_blob(sqliteStatement, index, bytes, Int32(data.count), SQLITE_TRANSIENT)
#if swift(>=5.0)
code = data.withUnsafeBytes {
sqlite3_bind_blob(sqliteStatement, index, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
}
#else
code = data.withUnsafeBytes {
sqlite3_bind_blob(sqliteStatement, index, $0, Int32(data.count), SQLITE_TRANSIENT)
}
#endif
}
// It looks like sqlite3_bind_xxx() functions do not access the file system.
@ -210,48 +213,30 @@ public class Statement {
}
}
// MARK: - AuthorizedStatement
// MARK: - Statement Preparation
/// A common protocol for UpdateStatement and SelectStatement
protocol AuthorizedStatement {
// This initializer should be a required initializer of Statement.
//
// But Swift requires this required initializer to be public:
// https://bugs.swift.org/browse/SR-2347
//
// We work around SR-2347 with this internal protocol.
//
// TODO: hasn't this bug been fixed in Swift 4.2? https://github.com/apple/swift/blob/master/CHANGELOG.md#swift-42
init(
database: Database,
statementStart: UnsafePointer<Int8>,
statementEnd: UnsafeMutablePointer<UnsafePointer<Int8>?>,
prepFlags: Int32,
authorizer: StatementCompilationAuthorizer) throws
}
extension AuthorizedStatement {
// Static function instead of an initializer because initializer doesn't
// compile due to the "capturing of an uninitialized self" in
// `sqlCodeUnits.withUnsafeBufferPointer`.
/// A common protocol for UpdateStatement and SelectStatement, only used as
/// support for SelectStatement.prepare(...) and UpdateStatement.prepare(...).
protocol StatementProtocol { }
extension Statement: StatementProtocol { }
extension StatementProtocol where Self: Statement {
// Static method instead of an initializer because initializer can't run
// inside `sqlCodeUnits.withUnsafeBufferPointer`.
static func prepare(sql: String, prepFlags: Int32, in database: Database) throws -> Self {
let authorizer = StatementCompilationAuthorizer()
database.authorizer = authorizer
defer { database.authorizer = nil }
let sqlCodeUnits = sql.utf8CString
return try sqlCodeUnits.withUnsafeBufferPointer { codeUnits in
let statementStart = UnsafePointer<Int8>(codeUnits.baseAddress)!
return try sql.utf8CString.withUnsafeBufferPointer { buffer in
let statementStart = buffer.baseAddress!
var statementEnd: UnsafePointer<Int8>? = nil
let statement: Self
do {
statement = try self.init(
database: database,
statementStart: statementStart,
statementEnd: &statementEnd,
prepFlags: prepFlags,
authorizer: authorizer)
} catch is EmptyStatementError {
guard let statement = try self.init(
database: database,
statementStart: statementStart,
statementEnd: &statementEnd,
prepFlags: prepFlags,
authorizer: authorizer) else
{
throw DatabaseError(
resultCode: .SQLITE_ERROR,
message: "empty statement",
@ -259,18 +244,11 @@ extension AuthorizedStatement {
arguments: nil)
}
let remainingData = Data(
bytesNoCopy: UnsafeMutableRawPointer(mutating: statementEnd!),
count: statementStart + sqlCodeUnits.count - statementEnd! - 1,
deallocator: .none)
let remainingSQL = String(data: remainingData, encoding: .utf8)!
.trimmingCharacters(in: statementSeparatorCharacterSet)
let remainingSQL = String(cString: statementEnd!).trimmingCharacters(in: .sqlStatementSeparators)
guard remainingSQL.isEmpty else {
throw DatabaseError(
resultCode: .SQLITE_MISUSE,
message: "Multiple statements found. To execute multiple statements, use Database.execute() instead.",
message: "Multiple statements found. To execute multiple statements, use Database.execute(sql:) instead.",
sql: sql,
arguments: nil)
}
@ -287,15 +265,16 @@ extension AuthorizedStatement {
/// You create SelectStatement with the Database.makeSelectStatement() method:
///
/// try dbQueue.read { db in
/// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM player WHERE score > ?")
/// let statement = try db.makeSelectStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?")
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
/// }
public final class SelectStatement : Statement {
/// The database region that the statement looks into.
public private(set) var databaseRegion: DatabaseRegion
public private(set) var databaseRegion = DatabaseRegion()
/// Creates a prepared statement.
/// Creates a prepared statement. Returns nil if the compiled string is
/// blank or empty.
///
/// - parameter database: A database connection.
/// - parameter statementStart: A pointer to a UTF-8 encoded C string
@ -305,21 +284,20 @@ public final class SelectStatement : Statement {
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
/// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html)
/// - authorizer: A StatementCompilationAuthorizer
/// - throws: DatabaseError in case of compilation error, and
/// EmptyStatementError if the compiled string is blank or empty.
init(
/// - throws: DatabaseError in case of compilation error.
required init?(
database: Database,
statementStart: UnsafePointer<Int8>,
statementEnd: UnsafeMutablePointer<UnsafePointer<Int8>?>,
prepFlags: Int32,
authorizer: StatementCompilationAuthorizer) throws
{
self.databaseRegion = DatabaseRegion()
try super.init(
database: database,
statementStart: statementStart,
statementEnd: statementEnd,
prepFlags: prepFlags)
prepFlags: prepFlags,
authorizer: authorizer)
GRDBPrecondition(authorizer.invalidatesDatabaseSchemaCache == false, "Invalid statement type for query \(String(reflecting: sql)): use UpdateStatement instead.")
GRDBPrecondition(authorizer.transactionEffect == nil, "Invalid statement type for query \(String(reflecting: sql)): use UpdateStatement instead.")
@ -351,65 +329,78 @@ public final class SelectStatement : Statement {
return columnIndexes[name.lowercased()]
}
/// Creates a cursor over the statement. This cursor does not produce any
/// value, and is only intended to give access to the sqlite3_step()
/// low-level function.
/// Creates a cursor over the statement which does not produce any
/// value. Each call to the next() cursor method calls the sqlite3_step()
/// C function.
func makeCursor(arguments: StatementArguments? = nil) -> StatementCursor {
return StatementCursor(statement: self, arguments: arguments)
}
/// Utility function for cursors
@usableFromInline
func reset(withArguments arguments: StatementArguments? = nil) {
prepare(withArguments: arguments)
try! reset()
}
/// Utility function for cursors
@usableFromInline
func didFail(withResultCode resultCode: Int32) throws -> Never {
database.selectStatementDidFail(self)
throw DatabaseError(
resultCode: resultCode,
message: database.lastErrorMessage,
sql: sql,
arguments: arguments)
}
}
// Hide AuthorizedStatement from Jazzy
extension SelectStatement: AuthorizedStatement { }
// TODO: remove public qualifier, or expose SelectStatement.makeCursor()
/// A cursor that iterates a database statement without producing any value.
/// Each call to the next() cursor method calls the sqlite3_step() C function.
///
/// For example:
///
/// try dbQueue.read { db in
/// let statement = db.makeSelectStatement("SELECT * FROM player")
/// let cursor: StatementCursor = statement.makeCursor()
/// let statement = db.makeSelectStatement(sql: "SELECT performSideEffect()")
/// let cursor = statement.makeCursor()
/// try cursor.next()
/// }
public final class StatementCursor: Cursor {
public let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private var done = false
final class StatementCursor: Cursor {
@usableFromInline let _statement: SelectStatement
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline var _done = false
// Use SelectStatement.cursor() instead
fileprivate init(statement: SelectStatement, arguments: StatementArguments? = nil) {
self.statement = statement
self.sqliteStatement = statement.sqliteStatement
statement.reset(withArguments: arguments)
// Use SelectStatement.makeCursor() instead
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil) {
_statement = statement
_sqliteStatement = statement.sqliteStatement
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Void? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
return .some(())
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}
@ -422,7 +413,7 @@ public final class StatementCursor: Cursor {
/// You create UpdateStatement with the Database.makeUpdateStatement() method:
///
/// try dbQueue.inTransaction { db in
/// let statement = try db.makeUpdateStatement("INSERT INTO player (name) VALUES (?)")
/// let statement = try db.makeUpdateStatement(sql: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
/// return .commit
@ -439,12 +430,13 @@ public final class UpdateStatement : Statement {
/// If true, the database schema cache gets invalidated after this statement
/// is executed.
private(set) var invalidatesDatabaseSchemaCache: Bool
private(set) var invalidatesDatabaseSchemaCache: Bool = false
private(set) var transactionEffect: TransactionEffect?
private(set) var databaseEventKinds: [DatabaseEventKind]
private(set) var databaseEventKinds: [DatabaseEventKind] = []
/// Creates a prepared statement.
/// Creates a prepared statement. Returns nil if the compiled string is
/// blank or empty.
///
/// - parameter database: A database connection.
/// - parameter statementStart: A pointer to a UTF-8 encoded C string
@ -454,22 +446,20 @@ public final class UpdateStatement : Statement {
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
/// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html)
/// - authorizer: A StatementCompilationAuthorizer
/// - throws: DatabaseError in case of compilation error, and
/// EmptyStatementError if the compiled string is blank or empty.
init(
/// - throws: DatabaseError in case of compilation error.
required init?(
database: Database,
statementStart: UnsafePointer<Int8>,
statementEnd: UnsafeMutablePointer<UnsafePointer<Int8>?>,
prepFlags: Int32,
authorizer: StatementCompilationAuthorizer) throws
{
self.invalidatesDatabaseSchemaCache = false
self.databaseEventKinds = []
try super.init(
database: database,
statementStart: statementStart,
statementEnd: statementEnd,
prepFlags: prepFlags)
prepFlags: prepFlags,
authorizer: authorizer)
self.invalidatesDatabaseSchemaCache = authorizer.invalidatesDatabaseSchemaCache
self.transactionEffect = authorizer.transactionEffect
self.databaseEventKinds = authorizer.databaseEventKinds
@ -477,7 +467,7 @@ public final class UpdateStatement : Statement {
/// Executes the SQL query.
///
/// - parameter arguments: Statement arguments.
/// - parameter arguments: Optional statement arguments.
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func execute(arguments: StatementArguments? = nil) throws {
SchedulingWatchdog.preconditionValidQueue(database)
@ -491,17 +481,17 @@ public final class UpdateStatement : Statement {
// The statement did return a row, and the user ignores the
// content of this row:
//
// try db.execute("SELECT ...")
// try db.execute(sql: "SELECT ...")
//
// That's OK: maybe the selected rows perform side effects.
// For example:
//
// try db.execute("SELECT sqlcipher_export(...)")
// try db.execute(sql: "SELECT sqlcipher_export(...)")
//
// Or maybe the user doesn't know that the executed statement
// return rows (https://github.com/groue/GRDB.swift/issues/15);
//
// try db.execute("PRAGMA journal_mode=WAL")
// try db.execute(sql: "PRAGMA journal_mode=WAL")
//
// It is thus important that we consume *all* rows.
continue
@ -518,10 +508,6 @@ public final class UpdateStatement : Statement {
}
}
// Hide AuthorizedStatement from Jazzy
extension UpdateStatement: AuthorizedStatement { }
// MARK: - StatementArguments
/// StatementArguments provide values to argument placeholders in raw
@ -542,12 +528,12 @@ extension UpdateStatement: AuthorizedStatement { }
/// To fill question marks placeholders, feed StatementArguments with an array:
///
/// db.execute(
/// "INSERT ... (?, ?)",
/// sql: "INSERT ... (?, ?)",
/// arguments: StatementArguments(["Arthur", 41]))
///
/// // Array literals are automatically converted:
/// db.execute(
/// "INSERT ... (?, ?)",
/// sql: "INSERT ... (?, ?)",
/// arguments: ["Arthur", 41])
///
/// ## Named Arguments
@ -555,12 +541,12 @@ extension UpdateStatement: AuthorizedStatement { }
/// To fill named arguments, feed StatementArguments with a dictionary:
///
/// db.execute(
/// "INSERT ... (:name, :score)",
/// sql: "INSERT ... (:name, :score)",
/// arguments: StatementArguments(["name": "Arthur", "score": 41]))
///
/// // Dictionary literals are automatically converted:
/// db.execute(
/// "INSERT ... (:name, :score)",
/// sql: "INSERT ... (:name, :score)",
/// arguments: ["name": "Arthur", "score": 41])
///
/// ## Concatenating Arguments
@ -570,7 +556,7 @@ extension UpdateStatement: AuthorizedStatement { }
///
/// var arguments: StatementArguments = ["Arthur"]
/// arguments += [41]
/// db.execute("INSERT ... (?, ?)", arguments: arguments)
/// db.execute(sql: "INSERT ... (?, ?)", arguments: arguments)
///
/// `+` and `+=` operators consider that overriding named arguments is a
/// programmer error:
@ -593,7 +579,7 @@ extension UpdateStatement: AuthorizedStatement { }
///
/// let sql = "SELECT ?2 AS two, :foo AS foo, ?1 AS one, :foo AS foo2, :bar AS bar"
/// var arguments: StatementArguments = [1, 2, "bar"] + ["foo": "foo"]
/// let row = try Row.fetchOne(db, sql, arguments: arguments)!
/// let row = try Row.fetchOne(db, sql: sql, arguments: arguments)!
/// print(row)
/// // Prints [two:2 foo:"foo" one:1 foo2:"foo" bar:"bar"]
///
@ -615,7 +601,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
// MARK: Empty Arguments
/// Creates empty StatementArguments.
init() {
public init() {
}
// MARK: Positional Arguments
@ -623,7 +609,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
/// Creates statement arguments from a sequence of optional values.
///
/// let values: [DatabaseValueConvertible?] = ["foo", 1, nil]
/// db.execute("INSERT ... (?,?,?)", arguments: StatementArguments(values))
/// db.execute(sql: "INSERT ... (?,?,?)", arguments: StatementArguments(values))
///
/// - parameter sequence: A sequence of DatabaseValueConvertible values.
/// - returns: A StatementArguments.
@ -634,7 +620,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
/// Creates statement arguments from a sequence of optional values.
///
/// let values: [String] = ["foo", "bar"]
/// db.execute("INSERT ... (?,?)", arguments: StatementArguments(values))
/// db.execute(sql: "INSERT ... (?,?)", arguments: StatementArguments(values))
///
/// - parameter sequence: A sequence of DatabaseValueConvertible values.
/// - returns: A StatementArguments.
@ -665,7 +651,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
/// such as a dictionary.
///
/// let values: [String: DatabaseValueConvertible?] = ["firstName": nil, "lastName": "Miller"]
/// db.execute("INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values))
/// db.execute(sql: "INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values))
///
/// - parameter sequence: A sequence of (key, value) pairs
/// - returns: A StatementArguments.
@ -677,7 +663,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
/// as a dictionary.
///
/// let values: [String: DatabaseValueConvertible?] = ["firstName": nil, "lastName": "Miller"]
/// db.execute("INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values))
/// db.execute(sql: "INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values))
///
/// - parameter sequence: A sequence of (key, value) pairs
/// - returns: A StatementArguments.
@ -863,7 +849,7 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
// MARK: Not Public
mutating func consume(_ statement: Statement, allowingRemainingValues: Bool) throws -> [DatabaseValue] {
mutating func extractBindings(forStatement statement: Statement, allowingRemainingValues: Bool) throws -> [DatabaseValue] {
let initialValuesCount = values.count
let bindings = try statement.sqliteArgumentNames.map { argumentName -> DatabaseValue in
if let argumentName = argumentName {
@ -893,7 +879,10 @@ public struct StatementArguments: CustomStringConvertible, Equatable, Expressibl
extension StatementArguments {
/// Returns a StatementArguments from an array literal:
///
/// db.selectRows("SELECT ...", arguments: ["Arthur", 41])
/// let arguments: StatementArguments = ["Arthur", 41]
/// try db.execute(
/// sql: "INSERT INTO player (name, score) VALUES (?, ?)"
/// arguments: arguments)
public init(arrayLiteral elements: DatabaseValueConvertible?...) {
self.init(elements)
}
@ -903,7 +892,10 @@ extension StatementArguments {
extension StatementArguments {
/// Returns a StatementArguments from a dictionary literal:
///
/// db.selectRows("SELECT ...", arguments: ["name": "Arthur", "score": 41])
/// let arguments: StatementArguments = ["name": "Arthur", "score": 41]
/// try db.execute(
/// sql: "INSERT INTO player (name, score) VALUES (:name, :score)"
/// arguments: arguments)
public init(dictionaryLiteral elements: (String, DatabaseValueConvertible?)...) {
self.init(elements)
}

View File

@ -13,11 +13,11 @@
/// DatabaseValueConvertible. GRDB will then automatically apply the
/// optimization whenever direct access to SQLite is possible:
///
/// let rows = Row.fetchCursor(db, "SELECT ...")
/// let rows = Row.fetchCursor(db, sql: "SELECT ...")
/// while let row = try rows.next() {
/// let int: Int = row[0] // there
/// }
/// let ints = Int.fetchAll(db, "SELECT ...") // there
/// let ints = Int.fetchAll(db, sql: "SELECT ...") // there
/// struct Player {
/// init(row: Row) {
/// name = row["name"] // there
@ -54,114 +54,110 @@ public protocol StatementColumnConvertible {
/// For example:
///
/// try dbQueue.read { db in
/// let names: ColumnCursor<String> = try String.fetchCursor(db, "SELECT name FROM player")
/// let names: ColumnCursor<String> = try String.fetchCursor(db, sql: "SELECT name FROM player")
/// while let name = names.next() { // String
/// print(name)
/// }
/// }
public final class FastDatabaseValueCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor {
private let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private let columnIndex: Int32
private var done = false
@usableFromInline let _statement: SelectStatement
@usableFromInline let _columnIndex: Int32
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.sqliteStatement = statement.sqliteStatement
_statement = statement
_sqliteStatement = statement.sqliteStatement
if let adapter = adapter {
// adapter may redefine the index of the leftmost column
self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
_columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
self.columnIndex = 0
_columnIndex = 0
}
statement.reset(withArguments: arguments)
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Value? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
return Value.fastDecode(from: sqliteStatement, index: columnIndex)
return Value.fastDecode(from: _sqliteStatement, atUncheckedIndex: _columnIndex)
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}
@available(*, deprecated, renamed: "FastDatabaseValueCursor")
public typealias ColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> = FastDatabaseValueCursor<Value>
/// A cursor of optional database values extracted from a single column.
/// For example:
///
/// try dbQueue.read { db in
/// let emails: NullableColumnCursor<String> = try Optional<String>.fetchCursor(db, "SELECT email FROM player")
/// let emails: NullableColumnCursor<String> = try Optional<String>.fetchCursor(db, sql: "SELECT email FROM player")
/// while let email = emails.next() { // String?
/// print(email ?? "<NULL>")
/// }
/// }
public final class FastNullableDatabaseValueCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor {
private let statement: SelectStatement
private let sqliteStatement: SQLiteStatement
private let columnIndex: Int32
private var done = false
@usableFromInline let _statement: SelectStatement
@usableFromInline let _columnIndex: Int32
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.sqliteStatement = statement.sqliteStatement
_statement = statement
_sqliteStatement = statement.sqliteStatement
if let adapter = adapter {
// adapter may redefine the index of the leftmost column
self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
_columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
self.columnIndex = 0
_columnIndex = 0
}
statement.reset(withArguments: arguments)
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Value?? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
return Value.fastDecodeIfPresent(from: sqliteStatement, atUncheckedIndex: columnIndex)
return Value.fastDecodeIfPresent(from: _sqliteStatement, atUncheckedIndex: _columnIndex)
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}
@available(*, deprecated, renamed: "FastNullableDatabaseValueCursor")
public typealias NullableColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> = FastNullableDatabaseValueCursor<Value>
/// Types that adopt both DatabaseValueConvertible and
/// StatementColumnConvertible can be efficiently initialized from
/// database values.
@ -174,7 +170,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// Returns a cursor over values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try String.fetchCursor(statement) // Cursor of String
/// while let name = try names.next() { // String
/// ...
@ -191,13 +187,14 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor<Self> {
return try FastDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try String.fetchAll(statement) // [String]
///
/// - parameters:
@ -206,13 +203,14 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - adapter: Optional RowAdapter
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let name = try String.fetchOne(statement) // String?
///
/// - parameters:
@ -221,6 +219,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
// fetchOne returns nil if there is no row, or if there is a row with a null value
let cursor = try FastNullableDatabaseValueCursor<Self>(statement: statement, arguments: arguments, adapter: adapter)
@ -234,7 +233,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// Returns a cursor over values fetched from an SQL query.
///
/// let names = try String.fetchCursor(db, "SELECT name FROM ...") // Cursor of String
/// let names = try String.fetchCursor(db, sql: "SELECT name FROM ...") // Cursor of String
/// while let name = try names.next() { // String
/// ...
/// }
@ -247,42 +246,45 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of values fetched from an SQL query.
///
/// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String]
/// let names = try String.fetchAll(db, sql: "SELECT name FROM ...") // [String]
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from an SQL query.
///
/// let name = try String.fetchOne(db, "SELECT name FROM ...") // String?
/// let name = try String.fetchOne(db, sql: "SELECT name FROM ...") // String?
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchOne(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -308,6 +310,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - request: A FetchRequest.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> FastDatabaseValueCursor<Self> {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -323,6 +326,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - request: A FetchRequest.
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Self] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -338,6 +342,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - request: A FetchRequest.
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
return try fetchOne(statement, adapter: adapter)
@ -364,6 +369,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementCol
/// - parameter db: A database connection.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> FastDatabaseValueCursor<RowDecoder> {
return try RowDecoder.fetchCursor(db, self)
}
@ -376,6 +382,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementCol
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
return try RowDecoder.fetchAll(db, self)
}
@ -391,6 +398,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementCol
/// - parameter db: A database connection.
/// - returns: An optional value.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchOne(_ db: Database) throws -> RowDecoder? {
return try RowDecoder.fetchOne(db, self)
}
@ -399,10 +407,10 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementCol
/// Swift's Optional comes with built-in methods that allow to fetch cursors
/// and arrays of optional DatabaseValueConvertible:
///
/// try Optional<String>.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String?]
/// try Optional<String>.fetchCursor(db, sql: "SELECT name FROM ...", arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(db, sql: "SELECT name FROM ...", arguments:...) // [String?]
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// try Optional<String>.fetchCursor(statement, arguments:...) // Cursor of String?
/// try Optional<String>.fetchAll(statement, arguments:...) // [String?]
///
@ -413,7 +421,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// Returns a cursor over optional values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try Optional<String>.fetchCursor(statement) // Cursor of String?
/// while let name = try names.next() { // String?
/// ...
@ -430,13 +438,14 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor<Wrapped> {
return try FastNullableDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of optional values fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT name FROM ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT name FROM ...")
/// let names = try Optional<String>.fetchAll(statement) // [String?]
///
/// - parameters:
@ -445,6 +454,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - adapter: Optional RowAdapter
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
@ -456,7 +466,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// Returns a cursor over optional values fetched from an SQL query.
///
/// let names = try Optional<String>.fetchCursor(db, "SELECT name FROM ...") // Cursor of String?
/// let names = try Optional<String>.fetchCursor(db, sql: "SELECT name FROM ...") // Cursor of String?
/// while let name = try names.next() { // String?
/// ...
/// }
@ -469,27 +479,29 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor<Wrapped> {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor<Wrapped> {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of optional values fetched from an SQL query.
///
/// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String?]
/// let names = try String.fetchAll(db, sql: "SELECT name FROM ...") // [String?]
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - parameter arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Wrapped?] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -515,6 +527,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - request: A FetchRequest.
/// - returns: A cursor over fetched optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> FastNullableDatabaseValueCursor<Wrapped> {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -530,6 +543,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - request: A FetchRequest.
/// - returns: An array of optional values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Wrapped?] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -556,6 +570,7 @@ extension FetchRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped:
/// - parameter db: A database connection.
/// - returns: A cursor over fetched values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> FastNullableDatabaseValueCursor<RowDecoder._Wrapped> {
return try Optional<RowDecoder._Wrapped>.fetchCursor(db, self)
}
@ -568,6 +583,7 @@ extension FetchRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped:
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [RowDecoder._Wrapped?] {
return try Optional<RowDecoder._Wrapped>.fetchAll(db, self)
}

View File

@ -7,6 +7,7 @@ import Foundation
/// Data is convertible to and from DatabaseValue.
extension Data : DatabaseValueConvertible, StatementColumnConvertible {
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
if let bytes = sqlite3_column_blob(sqliteStatement, index) {
let count = Int(sqlite3_column_bytes(sqliteStatement, index))

View File

@ -26,14 +26,14 @@ extension DatabaseValueConvertible where Self: ReferenceConvertible, Self.Refere
}
}
public extension DatabaseValueConvertible where Self: Decodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible {
extension DatabaseValueConvertible where Self: Decodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible {
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
// Preserve custom database decoding
return ReferenceType.fromDatabaseValue(databaseValue).flatMap { cast($0) }
}
}
public extension DatabaseValueConvertible where Self: Encodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible {
extension DatabaseValueConvertible where Self: Encodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
// Preserve custom database encoding
return (self as! ReferenceType).databaseValue

View File

@ -63,7 +63,8 @@ extension Date : DatabaseValueConvertible {
return nil
}
private init?(databaseDateComponents: DatabaseDateComponents) {
@usableFromInline
init?(databaseDateComponents: DatabaseDateComponents) {
guard databaseDateComponents.format.hasYMDComponents else {
// Refuse to turn hours without any date information into Date:
return nil
@ -122,6 +123,7 @@ extension Date: StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_INTEGER, SQLITE_FLOAT:

View File

@ -23,16 +23,20 @@ class SQLiteDateParser {
func components(cString: UnsafePointer<CChar>, length: Int) -> DatabaseDateComponents? {
assert(strlen(cString) == length)
// "HH:MM" is the shortest valid string
guard length >= 5 else { return nil }
if cString.advanced(by: 4).pointee == 45 /* '-' */ {
// "YYYY-..." -> datetime
if cString[4] == UInt8(ascii: "-") {
return datetimeComponents(cString: cString, length: length)
}
if cString.advanced(by: 2).pointee == 58 /* ':' */ {
// "HH-:..." -> time
if cString[2] == UInt8(ascii: ":") {
return timeComponents(cString: cString, length: length)
}
// Invalid
return nil
}

View File

@ -19,19 +19,15 @@ extension NSUUID: DatabaseValueConvertible {
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
switch dbValue.storage {
case .blob(let data) where data.count == 16:
// The code below works in debug configuration, but crashes in
// release configuration (Xcode 9.4.1)
// return data.withUnsafeBytes {
// self.init(uuidBytes: $0)
// }
// Workaround (involves a useless copy)
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: 16)
_ = data.copyBytes(to: buffer)
let uuid = self.init(uuidBytes: UnsafePointer(buffer.baseAddress!))
buffer.deallocate()
return uuid
#if swift(>=5.0)
return data.withUnsafeBytes {
self.init(uuidBytes: $0.bindMemory(to: UInt8.self).baseAddress)
}
#else
return data.withUnsafeBytes {
self.init(uuidBytes: $0)
}
#endif
case .string(let string):
return self.init(uuidString: string)
default:
@ -44,8 +40,7 @@ extension NSUUID: DatabaseValueConvertible {
/// UUID adopts DatabaseValueConvertible
extension UUID: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
var uuid_t = uuid
return withUnsafeBytes(of: &uuid_t) {
return withUnsafeBytes(of: uuid) {
Data(bytes: $0.baseAddress!, count: $0.count).databaseValue
}
}
@ -53,9 +48,15 @@ extension UUID: DatabaseValueConvertible {
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UUID? {
switch dbValue.storage {
case .blob(let data) where data.count == 16:
#if swift(>=5.0)
return data.withUnsafeBytes {
UUID(uuid: $0.bindMemory(to: uuid_t.self).first!)
}
#else
return data.withUnsafeBytes {
UUID(uuid: $0.pointee)
}
#endif
case .string(let string):
return UUID(uuidString: string)
default:
@ -65,6 +66,7 @@ extension UUID: DatabaseValueConvertible {
}
extension UUID: StatementColumnConvertible {
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_TEXT:

View File

@ -178,13 +178,13 @@ private struct DatabaseValueDecoder: Decoder {
}
}
public extension DatabaseValueConvertible where Self: Decodable {
extension DatabaseValueConvertible where Self: Decodable {
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
return try? self.init(from: DatabaseValueDecoder(dbValue: databaseValue, codingPath: []))
}
}
public extension DatabaseValueConvertible where Self: Decodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
extension DatabaseValueConvertible where Self: Decodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
// Preserve custom database decoding
return RawValue.fromDatabaseValue(databaseValue).flatMap { self.init(rawValue: $0) }

View File

@ -89,7 +89,7 @@ private struct DatabaseValueEncoder : Encoder {
}
}
public extension DatabaseValueConvertible where Self: Encodable {
extension DatabaseValueConvertible where Self: Encodable {
public var databaseValue: DatabaseValue {
var dbValue: DatabaseValue! = nil
let encoder = DatabaseValueEncoder(encode: { dbValue = $0 })
@ -98,7 +98,7 @@ public extension DatabaseValueConvertible where Self: Encodable {
}
}
public extension DatabaseValueConvertible where Self: Encodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
extension DatabaseValueConvertible where Self: Encodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
// Preserve custom database encoding
return rawValue.databaseValue

View File

@ -14,6 +14,7 @@ extension Bool: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
self = sqlite3_column_int64(sqliteStatement, index) != 0
}
@ -100,6 +101,7 @@ extension Int: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = Int(exactly: int64) {
@ -128,6 +130,7 @@ extension Int8: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = Int8(exactly: int64) {
@ -156,6 +159,7 @@ extension Int16: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = Int16(exactly: int64) {
@ -184,6 +188,7 @@ extension Int32: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = Int32(exactly: int64) {
@ -212,6 +217,7 @@ extension Int64: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
self = sqlite3_column_int64(sqliteStatement, index)
}
@ -244,6 +250,7 @@ extension UInt: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = UInt(exactly: int64) {
@ -272,6 +279,7 @@ extension UInt8: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = UInt8(exactly: int64) {
@ -300,6 +308,7 @@ extension UInt16: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = UInt16(exactly: int64) {
@ -328,6 +337,7 @@ extension UInt32: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = UInt32(exactly: int64) {
@ -356,6 +366,7 @@ extension UInt64: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
if let v = UInt64(exactly: int64) {
@ -384,6 +395,7 @@ extension Double: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
self = sqlite3_column_double(sqliteStatement, index)
}
@ -414,6 +426,7 @@ extension Float: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
self = Float(sqlite3_column_double(sqliteStatement, index))
}
@ -444,6 +457,7 @@ extension String: DatabaseValueConvertible, StatementColumnConvertible {
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inlinable
public init(sqliteStatement: SQLiteStatement, index: Int32) {
self = String(cString: sqlite3_column_text(sqliteStatement, index)!)
}
@ -543,7 +557,7 @@ extension DatabaseFunction {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedCapitalized)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public static let localizedCapitalize = DatabaseFunction("swiftLocalizedCapitalizedString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
@ -563,7 +577,7 @@ extension DatabaseFunction {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedLowercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public static let localizedLowercase = DatabaseFunction("swiftLocalizedLowercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
@ -583,7 +597,7 @@ extension DatabaseFunction {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedUppercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public static let localizedUppercase = DatabaseFunction("swiftLocalizedUppercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
@ -616,11 +630,11 @@ extension DatabaseCollation {
/// You can use it when creating database tables:
///
/// let collationName = DatabaseCollation.caseInsensitiveCompare.name
/// dbQueue.execute(
/// "CREATE TABLE players (" +
/// "name TEXT COLLATE \(collationName)" +
/// ")"
/// )
/// dbQueue.execute(sql: """
/// CREATE TABLE players (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
public static let unicodeCompare = DatabaseCollation("swiftCompare") { (lhs, rhs) in
return (lhs < rhs) ? .orderedAscending : ((lhs == rhs) ? .orderedSame : .orderedDescending)
}
@ -634,11 +648,11 @@ extension DatabaseCollation {
/// You can use it when creating database tables:
///
/// let collationName = DatabaseCollation.caseInsensitiveCompare.name
/// dbQueue.execute(
/// "CREATE TABLE players (" +
/// "name TEXT COLLATE \(collationName)" +
/// ")"
/// )
/// dbQueue.execute(sql: """
/// CREATE TABLE players (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
public static let caseInsensitiveCompare = DatabaseCollation("swiftCaseInsensitiveCompare") { (lhs, rhs) in
return lhs.caseInsensitiveCompare(rhs)
}
@ -652,11 +666,11 @@ extension DatabaseCollation {
/// You can use it when creating database tables:
///
/// let collationName = DatabaseCollation.localizedCaseInsensitiveCompare.name
/// dbQueue.execute(
/// "CREATE TABLE players (" +
/// "name TEXT COLLATE \(collationName)" +
/// ")"
/// )
/// dbQueue.execute(sql: """
/// CREATE TABLE players (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
public static let localizedCaseInsensitiveCompare = DatabaseCollation("swiftLocalizedCaseInsensitiveCompare") { (lhs, rhs) in
return lhs.localizedCaseInsensitiveCompare(rhs)
}
@ -670,11 +684,11 @@ extension DatabaseCollation {
/// You can use it when creating database tables:
///
/// let collationName = DatabaseCollation.localizedCompare.name
/// dbQueue.execute(
/// "CREATE TABLE players (" +
/// "name TEXT COLLATE \(collationName)" +
/// ")"
/// )
/// dbQueue.execute(sql: """
/// CREATE TABLE players (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
public static let localizedCompare = DatabaseCollation("swiftLocalizedCompare") { (lhs, rhs) in
return lhs.localizedCompare(rhs)
}
@ -688,11 +702,11 @@ extension DatabaseCollation {
/// You can use it when creating database tables:
///
/// let collationName = DatabaseCollation.localizedStandardCompare.name
/// dbQueue.execute(
/// "CREATE TABLE players (" +
/// "name TEXT COLLATE \(collationName)" +
/// ")"
/// )
/// dbQueue.execute(sql: """
/// CREATE TABLE players (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
public static let localizedStandardCompare = DatabaseCollation("swiftLocalizedStandardCompare") { (lhs, rhs) in
return lhs.localizedStandardCompare(rhs)
}

View File

@ -95,11 +95,11 @@ extension Database {
/// let observer = MyObserver()
/// dbQueue.add(transactionObserver: observer)
/// dbQueue.inDatabase { db in
/// try db.execute("BEGIN TRANSACTION")
/// try db.execute(sql: "BEGIN TRANSACTION")
///
/// Then a statement is executed:
///
/// try db.execute("INSERT INTO document ...")
/// try db.execute(sql: "INSERT INTO document ...")
///
/// The observation process starts when the statement is *compiled*:
/// sqlite3_set_authorizer tells that the statement performs insertion into the
@ -117,7 +117,7 @@ extension Database {
///
/// Now a savepoint is started:
///
/// try db.execute("SAVEPOINT foo")
/// try db.execute(sql: "SAVEPOINT foo")
///
/// Statement compilation has sqlite3_set_authorizer tell that this statement
/// begins a "foo" savepoint.
@ -128,7 +128,7 @@ extension Database {
///
/// Then another statement is executed:
///
/// try db.execute("INSERT INTO document ...")
/// try db.execute(sql: "INSERT INTO document ...")
///
/// This time, when the statement is *executed* and SQLite tells that a row has
/// been inserted, the broker buffers the change event instead of immediately
@ -138,7 +138,7 @@ extension Database {
///
/// The savepoint is released:
///
/// try db.execute("RELEASE SAVEPOINT foo")
/// try db.execute(sql: "RELEASE SAVEPOINT foo")
///
/// Statement compilation has sqlite3_set_authorizer tell that this statement
/// releases the "foo" savepoint.
@ -149,7 +149,7 @@ extension Database {
///
/// Finally the transaction is committed:
///
/// try db.execute("COMMIT")
/// try db.execute(sql: "COMMIT")
///
/// During the statement *execution*, SQlite tells the broker that the
/// transaction is about to be committed through sqlite3_commit_hook. The broker
@ -440,8 +440,8 @@ class DatabaseObservationBroker {
// SQLite, no transaction at all has started, and sqlite3_commit_hook
// was not triggered:
//
// try db.execute("BEGIN DEFERRED TRANSACTION")
// try db.execute("COMMIT") // <- no sqlite3_commit_hook callback invocation
// try db.execute(sql: "BEGIN DEFERRED TRANSACTION")
// try db.execute(sql: "COMMIT") // <- no sqlite3_commit_hook callback invocation
//
// Should we tell transaction observers of this transaction, or not?
// The code says that a transaction was open, but SQLite says the
@ -517,7 +517,7 @@ class DatabaseObservationBroker {
//
// But we have to deal with a particular case:
//
// let journalMode = String.fetchOne(db, "PRAGMA journal_mode = wal")
// let journalMode = String.fetchOne(db, sql: "PRAGMA journal_mode = wal")
//
// It runs a SelectStatement, not an UpdateStatement. But this not why
// this case is particular. What is unexpected is that it triggers

View File

@ -5,6 +5,25 @@
/// t.column("content")
/// }
public struct FTS3 : VirtualTableModule {
/// Options for Latin script characters. Matches the raw "remove_diacritics"
/// tokenizer argument.
///
/// See https://www.sqlite.org/fts3.html
public enum Diacritics {
/// Do not remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=0" tokenizer argument.
case keep
/// Remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=1" tokenizer argument.
case removeLegacy
#if GRDBCUSTOMSQLITE
/// Remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=2" tokenizer argument,
/// available from SQLite 3.27.0
case remove
#endif
}
/// Creates a FTS3 module suitable for the Database
/// `create(virtualTable:using:)` method.
///

View File

@ -19,8 +19,8 @@ public struct FTS3Pattern {
// that pattern.
do {
try DatabaseQueue().inDatabase { db in
try db.execute("CREATE VIRTUAL TABLE documents USING fts3()")
try db.execute("SELECT * FROM documents WHERE content MATCH ?", arguments: [rawPattern])
try db.execute(sql: "CREATE VIRTUAL TABLE documents USING fts3()")
try db.execute(sql: "SELECT * FROM documents WHERE content MATCH ?", arguments: [rawPattern])
}
} catch let error as DatabaseError {
// Remove private SQL & arguments from the thrown error
@ -78,7 +78,7 @@ public struct FTS3Pattern {
/// FTS3Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar
///
/// - parameter string: The string to turn into an FTS3 pattern
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
public init?(matchingAnyTokenIn string: String) {
let tokens = FTS3TokenizerDescriptor.simple.tokenize(string)
guard !tokens.isEmpty else { return nil }
@ -92,7 +92,7 @@ public struct FTS3Pattern {
/// FTS3Pattern(matchingAllTokensIn: "foo bar") // foo bar
///
/// - parameter string: The string to turn into an FTS3 pattern
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
public init?(matchingAllTokensIn string: String) {
let tokens = FTS3TokenizerDescriptor.simple.tokenize(string)
guard !tokens.isEmpty else { return nil }
@ -106,7 +106,7 @@ public struct FTS3Pattern {
/// FTS3Pattern(matchingPhrase: "foo bar") // "foo bar"
///
/// - parameter string: The string to turn into an FTS3 pattern
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
public init?(matchingPhrase string: String) {
let tokens = FTS3TokenizerDescriptor.simple.tokenize(string)
guard !tokens.isEmpty else { return nil }

View File

@ -40,16 +40,16 @@ public struct FTS3TokenizerDescriptor {
/// }
///
/// - parameters:
/// - removeDiacritics: If true (the default), then SQLite will strip
/// diacritics from latin characters.
/// - diacritics: By default SQLite will strip diacritics from
/// latin characters.
/// - separators: Unless empty (the default), SQLite will consider these
/// characters as token separators.
/// - tokenCharacters: Unless empty (the default), SQLite will consider
/// these characters as token characters.
///
/// See https://www.sqlite.org/fts3.html#tokenizer
public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
return _unicode61(removeDiacritics: removeDiacritics, separators: separators, tokenCharacters: tokenCharacters)
public static func unicode61(diacritics: FTS3.Diacritics = .removeLegacy, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
return _unicode61(diacritics: diacritics, separators: separators, tokenCharacters: tokenCharacters)
}
#else
/// The "unicode61" tokenizer.
@ -59,26 +59,33 @@ public struct FTS3TokenizerDescriptor {
/// }
///
/// - parameters:
/// - removeDiacritics: If true (the default), then SQLite will strip
/// diacritics from latin characters.
/// - diacritics: By default SQLite will strip diacritics from
/// latin characters.
/// - separators: Unless empty (the default), SQLite will consider these
/// characters as token separators.
/// - tokenCharacters: Unless empty (the default), SQLite will consider
/// these characters as token characters.
///
/// See https://www.sqlite.org/fts3.html#tokenizer
@available(iOS 8.2, OSX 10.10, *)
public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
@available(OSX 10.10, *)
public static func unicode61(diacritics: FTS3.Diacritics = .removeLegacy, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
// query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
return _unicode61(removeDiacritics: removeDiacritics, separators: separators, tokenCharacters: tokenCharacters)
return _unicode61(diacritics: diacritics, separators: separators, tokenCharacters: tokenCharacters)
}
#endif
private static func _unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
private static func _unicode61(diacritics: FTS3.Diacritics, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor {
var arguments: [String] = []
if !removeDiacritics {
switch diacritics {
case .removeLegacy:
break
case .keep:
arguments.append("remove_diacritics=0")
#if GRDBCUSTOMSQLITE
case .remove:
arguments.append("remove_diacritics=2")
#endif
}
if !separators.isEmpty {
// TODO: test "=" and "\"", "(" and ")" as separators, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:)
@ -96,7 +103,7 @@ public struct FTS3TokenizerDescriptor {
return _tokenize(string)
}
#else
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
func tokenize(_ string: String) -> [String] {
return _tokenize(string)
}
@ -116,8 +123,8 @@ public struct FTS3TokenizerDescriptor {
}
let tokenizerSQL = tokenizerChunks.joined(separator: ", ")
// Assume fts3tokenize virtual table in an in-memory database always succeeds
try! db.execute("CREATE VIRTUAL TABLE tokens USING fts3tokenize(\(tokenizerSQL))")
return try! String.fetchAll(db, "SELECT token FROM tokens WHERE input = ? ORDER BY position", arguments: [string])
try! db.execute(sql: "CREATE VIRTUAL TABLE tokens USING fts3tokenize(\(tokenizerSQL))")
return try! String.fetchAll(db, sql: "SELECT token FROM tokens WHERE input = ? ORDER BY position", arguments: [string])
}
}
}

View File

@ -110,7 +110,7 @@ public struct FTS4 : VirtualTableModule {
let oldRowID = "old.\(rowIDColumn.quotedDatabaseIdentifier)"
try db.execute("""
try db.execute(sql: """
CREATE TRIGGER \("__\(tableName)_bu".quotedDatabaseIdentifier) BEFORE UPDATE ON \(content) BEGIN
DELETE FROM \(ftsTable) WHERE docid=\(oldRowID);
END;
@ -127,7 +127,7 @@ public struct FTS4 : VirtualTableModule {
// https://www.sqlite.org/fts3.html#*fts4rebuidcmd
try db.execute("INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')")
try db.execute(sql: "INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')")
}
}
}
@ -288,7 +288,7 @@ public final class FTS4ColumnDefinition {
/// See https://www.sqlite.org/fts3.html#the_notindexed_option
///
/// - returns: Self so that you can further refine the column definition.
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
@discardableResult
public func notIndexed() -> Self {
// notindexed FTS4 option was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0
@ -318,7 +318,7 @@ public final class FTS4ColumnDefinition {
extension Database {
/// Deletes the synchronization triggers for a synchronized FTS4 table
public func dropFTS4SynchronizationTriggers(forTable tableName: String) throws {
try execute("""
try execute(sql: """
DROP TRIGGER IF EXISTS \("__\(tableName)_bu".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_bd".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_au".quotedDatabaseIdentifier);

View File

@ -1,5 +1,34 @@
#if SQLITE_ENABLE_FTS5
/// FTS5 lets you define "fts5" virtual tables.
/// FTS5 lets you define "fts5" virtual tables.
///
/// // CREATE VIRTUAL TABLE document USING fts5(content)
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content")
/// }
///
/// See https://www.sqlite.org/fts5.html
public struct FTS5 : VirtualTableModule {
/// Options for Latin script characters. Matches the raw "remove_diacritics"
/// tokenizer argument.
///
/// See https://www.sqlite.org/fts5.html
public enum Diacritics {
/// Do not remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=0" tokenizer argument.
case keep
/// Remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=1" tokenizer argument.
case removeLegacy
#if GRDBCUSTOMSQLITE
/// Remove diacritics from Latin script characters. This
/// option matches the raw "remove_diacritics=2" tokenizer argument,
/// available from SQLite 3.27.0
case remove
#endif
}
/// Creates a FTS5 module suitable for the Database
/// `create(virtualTable:using:)` method.
///
/// // CREATE VIRTUAL TABLE document USING fts5(content)
/// try db.create(virtualTable: "document", using: FTS5()) { t in
@ -7,391 +36,364 @@
/// }
///
/// See https://www.sqlite.org/fts5.html
public struct FTS5 : VirtualTableModule {
public init() {
}
// MARK: - VirtualTableModule Adoption
/// The virtual table module name
public let moduleName = "fts5"
/// Don't use this method.
public func makeTableDefinition() -> FTS5TableDefinition {
return FTS5TableDefinition()
}
/// Don't use this method.
public func moduleArguments(for definition: FTS5TableDefinition, in db: Database) throws -> [String] {
var arguments: [String] = []
/// Creates a FTS5 module suitable for the Database
/// `create(virtualTable:using:)` method.
///
/// // CREATE VIRTUAL TABLE document USING fts5(content)
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content")
/// }
///
/// See https://www.sqlite.org/fts5.html
public init() {
if definition.columns.isEmpty {
// Programmer error
fatalError("FTS5 virtual table requires at least one column.")
}
// MARK: - VirtualTableModule Adoption
/// The virtual table module name
public let moduleName = "fts5"
/// Don't use this method.
public func makeTableDefinition() -> FTS5TableDefinition {
return FTS5TableDefinition()
}
/// Don't use this method.
public func moduleArguments(for definition: FTS5TableDefinition, in db: Database) throws -> [String] {
var arguments: [String] = []
if definition.columns.isEmpty {
// Programmer error
fatalError("FTS5 virtual table requires at least one column.")
}
for column in definition.columns {
if column.isIndexed {
arguments.append("\(column.name)")
} else {
arguments.append("\(column.name) UNINDEXED")
}
}
if let tokenizer = definition.tokenizer {
arguments.append("tokenize=\(tokenizer.components.joined(separator: " ").sqlExpression.sql)")
}
switch definition.contentMode {
case .raw(let content, let contentRowID):
if let content = content {
arguments.append("content=\(content.sqlExpression.sql)")
}
if let contentRowID = contentRowID {
arguments.append("content_rowid=\(contentRowID.sqlExpression.sql)")
}
case .synchronized(let contentTable):
arguments.append("content=\(contentTable.sqlExpression.sql)")
if let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn {
arguments.append("content_rowid=\(rowIDColumn.sqlExpression.sql)")
}
}
if let prefixes = definition.prefixes {
arguments.append("prefix=\(prefixes.sorted().map { "\($0)" }.joined(separator: " ").sqlExpression.sql)")
}
if let columnSize = definition.columnSize {
arguments.append("columnSize=\(columnSize)")
}
if let detail = definition.detail {
arguments.append("detail=\(detail)")
}
return arguments
}
/// Reserved; part of the VirtualTableModule protocol.
///
/// See Database.create(virtualTable:using:)
public func database(_ db: Database, didCreate tableName: String, using definition: FTS5TableDefinition) throws {
switch definition.contentMode {
case .raw:
break
case .synchronized(let contentTable):
// https://sqlite.org/fts5.html#external_content_tables
let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn ?? Column.rowID.name
let ftsTable = tableName.quotedDatabaseIdentifier
let content = contentTable.quotedDatabaseIdentifier
let indexedColumns = definition.columns.map { $0.name }
let ftsColumns = (["rowid"] + indexedColumns)
.map { $0.quotedDatabaseIdentifier }
.joined(separator: ", ")
let newContentColumns = ([rowIDColumn] + indexedColumns)
.map { "new.\($0.quotedDatabaseIdentifier)" }
.joined(separator: ", ")
let oldContentColumns = ([rowIDColumn] + indexedColumns)
.map { "old.\($0.quotedDatabaseIdentifier)" }
.joined(separator: ", ")
try db.execute("""
CREATE TRIGGER \("__\(tableName)_ai".quotedDatabaseIdentifier) AFTER INSERT ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns));
END;
CREATE TRIGGER \("__\(tableName)_ad".quotedDatabaseIdentifier) AFTER DELETE ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns));
END;
CREATE TRIGGER \("__\(tableName)_au".quotedDatabaseIdentifier) AFTER UPDATE ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns));
INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns));
END;
""")
// https://sqlite.org/fts5.html#the_rebuild_command
try db.execute("INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')")
for column in definition.columns {
if column.isIndexed {
arguments.append("\(column.name)")
} else {
arguments.append("\(column.name) UNINDEXED")
}
}
static func api(_ db: Database) -> UnsafePointer<fts5_api> {
// Access to FTS5 is one of the rare SQLite api which was broken in
// SQLite 3.20.0+, for security reasons:
//
// Starting SQLite 3.20.0+, we need to use the new sqlite3_bind_pointer api.
// The previous way to access FTS5 does not work any longer.
//
// So let's see which SQLite version we are linked against:
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// GRDB is linked against SQLCipher or a custom SQLite build: SQLite 3.20.0 or more.
return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
#else
// GRDB is linked against the system SQLite.
//
// Do we use SQLite 3.19.3 (iOS 11.4), or SQLite 3.24.0 (iOS 12.0)?
// We need to check for available(iOS 12.0, OSX 10.14, watchOS 5.0, *).
//
// This test requires the Swift 4.2 compiler.
//
// It does not need the Swift 4.2 language, though: the Swift 4.2
// compiler running in Swift 4.0 compatibility mode is OK.
//
// On top of that, we want to preserve compatibility with Xcode 9.3+.
//
// So let's check exactly which compiler version we are using.
//
// Fortunately, this horribly complex check has been solved
// by @hartbit: see https://forums.swift.org/t/compiler-version-directive/11952
// and https://github.com/hartbit/swift-evolution/blob/compiler-directive/proposals/XXXX-compiler-version-directive.md
#if swift(>=4.1.50) || (swift(>=3.4) && !swift(>=4.0))
if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
// SQLite 3.24.0 or more
// setup: Xcode 10.0, SWIFT_VERSION = 4.0, iOS 12
// setup: Xcode 10.0, SWIFT_VERSION = 4.2, iOS 12
return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
} else {
// SQLite 3.19.3 or less
// setup: Xcode 10.0, SWIFT_VERSION = 4.0, iOS 11
// setup: Xcode 10.0, SWIFT_VERSION = 4.2, iOS 11
return api_v1(db)
}
#else
// SQLite 3.19.3 or less
// setup: Xcode 9.4.1, iOS 11
return api_v1(db)
#endif
#endif
if let tokenizer = definition.tokenizer {
arguments.append("tokenize=\(tokenizer.components.joined(separator: " ").sqlExpression.quotedSQL())")
}
private static func api_v1(_ db: Database) -> UnsafePointer<fts5_api> {
guard let data = try! Data.fetchOne(db, "SELECT fts5()") else {
fatalError("FTS5 is not available")
switch definition.contentMode {
case .raw(let content, let contentRowID):
if let content = content {
arguments.append("content=\(content.sqlExpression.quotedSQL())")
}
if let contentRowID = contentRowID {
arguments.append("content_rowid=\(contentRowID.sqlExpression.quotedSQL())")
}
case .synchronized(let contentTable):
arguments.append("content=\(contentTable.sqlExpression.quotedSQL())")
if let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn {
arguments.append("content_rowid=\(rowIDColumn.sqlExpression.quotedSQL())")
}
return data.withUnsafeBytes { $0.pointee }
}
// Technique given by Jordan Rose:
// https://forums.swift.org/t/c-interoperability-combinations-of-library-and-os-versions/14029/4
private static func api_v2(
_ db: Database,
_ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer<Int8>?, Int32, UInt32, UnsafeMutablePointer<OpaquePointer?>?, UnsafeMutablePointer<UnsafePointer<Int8>?>?) -> Int32,
_ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, Int32, UnsafeMutableRawPointer?, UnsafePointer<Int8>?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> Int32)
-> UnsafePointer<fts5_api>
{
let sqliteConnection = db.sqliteConnection
var statement: SQLiteStatement? = nil
var api: UnsafePointer<fts5_api>? = nil
let type: StaticString = "fts5_api_ptr"
if let prefixes = definition.prefixes {
arguments.append("prefix=\(prefixes.sorted().map { "\($0)" }.joined(separator: " ").sqlExpression.quotedSQL())")
}
if let columnSize = definition.columnSize {
arguments.append("columnSize=\(columnSize)")
}
if let detail = definition.detail {
arguments.append("detail=\(detail)")
}
return arguments
}
/// Reserved; part of the VirtualTableModule protocol.
///
/// See Database.create(virtualTable:using:)
public func database(_ db: Database, didCreate tableName: String, using definition: FTS5TableDefinition) throws {
switch definition.contentMode {
case .raw:
break
case .synchronized(let contentTable):
// https://sqlite.org/fts5.html#external_content_tables
let code = sqlite3_prepare_v3(db.sqliteConnection, "SELECT fts5(?)", -1, 0, &statement, nil)
guard code == SQLITE_OK else {
fatalError("FTS5 is not available")
}
defer { sqlite3_finalize(statement) }
type.utf8Start.withMemoryRebound(to: Int8.self, capacity: type.utf8CodeUnitCount) { typePointer in
_ = sqlite3_bind_pointer(statement, 1, &api, typePointer, nil)
}
sqlite3_step(statement)
guard let result = api else {
fatalError("FTS5 is not available")
}
return result
let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn ?? Column.rowID.name
let ftsTable = tableName.quotedDatabaseIdentifier
let content = contentTable.quotedDatabaseIdentifier
let indexedColumns = definition.columns.map { $0.name }
let ftsColumns = (["rowid"] + indexedColumns)
.map { $0.quotedDatabaseIdentifier }
.joined(separator: ", ")
let newContentColumns = ([rowIDColumn] + indexedColumns)
.map { "new.\($0.quotedDatabaseIdentifier)" }
.joined(separator: ", ")
let oldContentColumns = ([rowIDColumn] + indexedColumns)
.map { "old.\($0.quotedDatabaseIdentifier)" }
.joined(separator: ", ")
try db.execute(sql: """
CREATE TRIGGER \("__\(tableName)_ai".quotedDatabaseIdentifier) AFTER INSERT ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns));
END;
CREATE TRIGGER \("__\(tableName)_ad".quotedDatabaseIdentifier) AFTER DELETE ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns));
END;
CREATE TRIGGER \("__\(tableName)_au".quotedDatabaseIdentifier) AFTER UPDATE ON \(content) BEGIN
INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns));
INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns));
END;
""")
// https://sqlite.org/fts5.html#the_rebuild_command
try db.execute(sql: "INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')")
}
}
/// The FTS5TableDefinition class lets you define columns of a FTS5 virtual table.
static func api(_ db: Database) -> UnsafePointer<fts5_api> {
// Access to FTS5 is one of the rare SQLite api which was broken in
// SQLite 3.20.0+, for security reasons:
//
// Starting SQLite 3.20.0+, we need to use the new sqlite3_bind_pointer api.
// The previous way to access FTS5 does not work any longer.
//
// So let's see which SQLite version we are linked against:
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// GRDB is linked against SQLCipher or a custom SQLite build: SQLite 3.20.0 or more.
return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
#else
// GRDB is linked against the system SQLite.
//
// Do we use SQLite 3.19.3 (iOS 11.4), or SQLite 3.24.0 (iOS 12.0)?
if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
// SQLite 3.24.0 or more
return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
} else {
// SQLite 3.19.3 or less
return api_v1(db)
}
#endif
}
private static func api_v1(_ db: Database) -> UnsafePointer<fts5_api> {
guard let data = try! Data.fetchOne(db, sql: "SELECT fts5()") else {
fatalError("FTS5 is not available")
}
#if swift(>=5.0)
return data.withUnsafeBytes {
$0.bindMemory(to: UnsafePointer<fts5_api>.self).first!
}
#else
return data.withUnsafeBytes {
$0.pointee
}
#endif
}
// Technique given by Jordan Rose:
// https://forums.swift.org/t/c-interoperability-combinations-of-library-and-os-versions/14029/4
private static func api_v2(
_ db: Database,
_ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer<Int8>?, Int32, UInt32, UnsafeMutablePointer<OpaquePointer?>?, UnsafeMutablePointer<UnsafePointer<Int8>?>?) -> Int32,
_ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, Int32, UnsafeMutableRawPointer?, UnsafePointer<Int8>?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> Int32)
-> UnsafePointer<fts5_api>
{
let sqliteConnection = db.sqliteConnection
var statement: SQLiteStatement? = nil
var api: UnsafePointer<fts5_api>? = nil
let type: StaticString = "fts5_api_ptr"
let code = sqlite3_prepare_v3(db.sqliteConnection, "SELECT fts5(?)", -1, 0, &statement, nil)
guard code == SQLITE_OK else {
fatalError("FTS5 is not available")
}
defer { sqlite3_finalize(statement) }
type.utf8Start.withMemoryRebound(to: Int8.self, capacity: type.utf8CodeUnitCount) { typePointer in
_ = sqlite3_bind_pointer(statement, 1, &api, typePointer, nil)
}
sqlite3_step(statement)
guard let result = api else {
fatalError("FTS5 is not available")
}
return result
}
}
/// The FTS5TableDefinition class lets you define columns of a FTS5 virtual table.
///
/// You don't create instances of this class. Instead, you use the Database
/// `create(virtualTable:using:)` method:
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in // t is FTS5TableDefinition
/// t.column("content")
/// }
///
/// See https://www.sqlite.org/fts5.html
public final class FTS5TableDefinition {
enum ContentMode {
case raw(content: String?, contentRowID: String?)
case synchronized(contentTable: String)
}
fileprivate var columns: [FTS5ColumnDefinition] = []
fileprivate var contentMode: ContentMode = .raw(content: nil, contentRowID: nil)
/// The virtual table tokenizer
///
/// You don't create instances of this class. Instead, you use the Database
/// `create(virtualTable:using:)` method:
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.tokenizer = .porter()
/// }
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in // t is FTS5TableDefinition
/// See https://www.sqlite.org/fts5.html#fts5_table_creation_and_initialization
public var tokenizer: FTS5TokenizerDescriptor?
/// The FTS5 `content` option
///
/// When you want the full-text table to be synchronized with the
/// content of an external table, prefer the `synchronize(withTable:)`
/// method.
///
/// Setting this property invalidates any synchronization previously
/// established with the `synchronize(withTable:)` method.
///
/// See https://www.sqlite.org/fts5.html#external_content_and_contentless_tables
public var content: String? {
get {
switch contentMode {
case .raw(let content, _):
return content
case .synchronized(let contentTable):
return contentTable
}
}
set {
switch contentMode {
case .raw(_, let contentRowID):
contentMode = .raw(content: newValue, contentRowID: contentRowID)
case .synchronized:
contentMode = .raw(content: newValue, contentRowID: nil)
}
}
}
/// The FTS5 `content_rowid` option
///
/// When you want the full-text table to be synchronized with the
/// content of an external table, prefer the `synchronize(withTable:)`
/// method.
///
/// Setting this property invalidates any synchronization previously
/// established with the `synchronize(withTable:)` method.
///
/// See https://sqlite.org/fts5.html#external_content_tables
public var contentRowID: String? {
get {
switch contentMode {
case .raw(_, let contentRowID):
return contentRowID
case .synchronized:
return nil
}
}
set {
switch contentMode {
case .raw(let content, _):
contentMode = .raw(content: content, contentRowID: newValue)
case .synchronized:
contentMode = .raw(content: nil, contentRowID: newValue)
}
}
}
/// Support for the FTS5 `prefix` option
///
/// See https://www.sqlite.org/fts5.html#prefix_indexes
public var prefixes: Set<Int>?
/// Support for the FTS5 `columnsize` option
///
/// https://www.sqlite.org/fts5.html#the_columnsize_option
public var columnSize: Int?
/// Support for the FTS5 `detail` option
///
/// https://www.sqlite.org/fts5.html#the_detail_option
public var detail: String?
/// Appends a table column.
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content")
/// }
///
/// See https://www.sqlite.org/fts5.html
public final class FTS5TableDefinition {
enum ContentMode {
case raw(content: String?, contentRowID: String?)
case synchronized(contentTable: String)
}
fileprivate var columns: [FTS5ColumnDefinition] = []
fileprivate var contentMode: ContentMode = .raw(content: nil, contentRowID: nil)
/// The virtual table tokenizer
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.tokenizer = .porter()
/// }
///
/// See https://www.sqlite.org/fts5.html#fts5_table_creation_and_initialization
public var tokenizer: FTS5TokenizerDescriptor?
/// The FTS5 `content` option
///
/// When you want the full-text table to be synchronized with the
/// content of an external table, prefer the `synchronize(withTable:)`
/// method.
///
/// Setting this property invalidates any synchronization previously
/// established with the `synchronize(withTable:)` method.
///
/// See https://www.sqlite.org/fts5.html#external_content_and_contentless_tables
public var content: String? {
get {
switch contentMode {
case .raw(let content, _):
return content
case .synchronized(let contentTable):
return contentTable
}
}
set {
switch contentMode {
case .raw(_, let contentRowID):
contentMode = .raw(content: newValue, contentRowID: contentRowID)
case .synchronized:
contentMode = .raw(content: newValue, contentRowID: nil)
}
}
}
/// The FTS5 `content_rowid` option
///
/// When you want the full-text table to be synchronized with the
/// content of an external table, prefer the `synchronize(withTable:)`
/// method.
///
/// Setting this property invalidates any synchronization previously
/// established with the `synchronize(withTable:)` method.
///
/// See https://sqlite.org/fts5.html#external_content_tables
public var contentRowID: String? {
get {
switch contentMode {
case .raw(_, let contentRowID):
return contentRowID
case .synchronized:
return nil
}
}
set {
switch contentMode {
case .raw(let content, _):
contentMode = .raw(content: content, contentRowID: newValue)
case .synchronized:
contentMode = .raw(content: nil, contentRowID: newValue)
}
}
}
/// Support for the FTS5 `prefix` option
///
/// See https://www.sqlite.org/fts5.html#prefix_indexes
public var prefixes: Set<Int>?
/// Support for the FTS5 `columnsize` option
///
/// https://www.sqlite.org/fts5.html#the_columnsize_option
public var columnSize: Int?
/// Support for the FTS5 `detail` option
///
/// https://www.sqlite.org/fts5.html#the_detail_option
public var detail: String?
/// Appends a table column.
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content")
/// }
///
/// - parameter name: the column name.
@discardableResult
public func column(_ name: String) -> FTS5ColumnDefinition {
let column = FTS5ColumnDefinition(name: name)
columns.append(column)
return column
}
/// Synchronizes the full-text table with the content of an external
/// table.
///
/// The full-text table is initially populated with the existing
/// content in the external table. SQL triggers make sure that the
/// full-text table is kept up to date with the external table.
///
/// See https://sqlite.org/fts5.html#external_content_tables
public func synchronize(withTable tableName: String) {
contentMode = .synchronized(contentTable: tableName)
}
/// - parameter name: the column name.
@discardableResult
public func column(_ name: String) -> FTS5ColumnDefinition {
let column = FTS5ColumnDefinition(name: name)
columns.append(column)
return column
}
/// The FTS5ColumnDefinition class lets you refine a column of an FTS5
/// virtual table.
/// Synchronizes the full-text table with the content of an external
/// table.
///
/// You get instances of this class when you create an FTS5 table:
/// The full-text table is initially populated with the existing
/// content in the external table. SQL triggers make sure that the
/// full-text table is kept up to date with the external table.
///
/// See https://sqlite.org/fts5.html#external_content_tables
public func synchronize(withTable tableName: String) {
contentMode = .synchronized(contentTable: tableName)
}
}
/// The FTS5ColumnDefinition class lets you refine a column of an FTS5
/// virtual table.
///
/// You get instances of this class when you create an FTS5 table:
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content") // FTS5ColumnDefinition
/// }
///
/// See https://www.sqlite.org/fts5.html
public final class FTS5ColumnDefinition {
fileprivate let name: String
fileprivate var isIndexed: Bool
init(name: String) {
self.name = name
self.isIndexed = true
}
/// Excludes the column from the full-text index.
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("content") // FTS5ColumnDefinition
/// t.column("a")
/// t.column("b").notIndexed()
/// }
///
/// See https://www.sqlite.org/fts5.html
public final class FTS5ColumnDefinition {
fileprivate let name: String
fileprivate var isIndexed: Bool
init(name: String) {
self.name = name
self.isIndexed = true
}
/// Excludes the column from the full-text index.
///
/// try db.create(virtualTable: "document", using: FTS5()) { t in
/// t.column("a")
/// t.column("b").notIndexed()
/// }
///
/// See https://www.sqlite.org/fts5.html#the_unindexed_column_option
///
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func notIndexed() -> Self {
self.isIndexed = false
return self
}
}
extension Column {
/// The FTS5 rank column
public static let rank = Column("rank")
/// See https://www.sqlite.org/fts5.html#the_unindexed_column_option
///
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func notIndexed() -> Self {
self.isIndexed = false
return self
}
}
extension Database {
/// Deletes the synchronization triggers for a synchronized FTS5 table
public func dropFTS5SynchronizationTriggers(forTable tableName: String) throws {
try execute("""
DROP TRIGGER IF EXISTS \("__\(tableName)_ai".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_ad".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_au".quotedDatabaseIdentifier);
""")
}
extension Column {
/// The FTS5 rank column
public static let rank = Column("rank")
}
extension Database {
/// Deletes the synchronization triggers for a synchronized FTS5 table
public func dropFTS5SynchronizationTriggers(forTable tableName: String) throws {
try execute(sql: """
DROP TRIGGER IF EXISTS \("__\(tableName)_ai".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_ad".quotedDatabaseIdentifier);
DROP TRIGGER IF EXISTS \("__\(tableName)_au".quotedDatabaseIdentifier);
""")
}
}
#endif

View File

@ -1,153 +1,153 @@
#if SQLITE_ENABLE_FTS5
/// The protocol for custom FTS5 tokenizers.
public protocol FTS5CustomTokenizer : FTS5Tokenizer {
/// The name of the tokenizer; should uniquely identify your custom
/// tokenizer.
static var name: String { get }
/// Creates a custom tokenizer.
///
/// The arguments parameter is an array of String built from the CREATE
/// VIRTUAL TABLE statement. In the example below, the arguments will
/// be `["arg1", "arg2"]`.
///
/// CREATE VIRTUAL TABLE document USING fts5(
/// tokenize='custom arg1 arg2'
/// )
///
/// - parameter db: A Database connection
/// - parameter arguments: An array of string arguments
init(db: Database, arguments: [String]) throws
}
/// The protocol for custom FTS5 tokenizers.
public protocol FTS5CustomTokenizer : FTS5Tokenizer {
/// The name of the tokenizer; should uniquely identify your custom
/// tokenizer.
static var name: String { get }
extension FTS5CustomTokenizer {
/// Creates an FTS5 tokenizer descriptor.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// let tokenizer = MyTokenizer.tokenizerDescriptor(arguments: ["unicode61", "remove_diacritics", "0"])
/// t.tokenizer = tokenizer
/// }
public static func tokenizerDescriptor(arguments: [String] = []) -> FTS5TokenizerDescriptor {
return FTS5TokenizerDescriptor(components: [name] + arguments)
}
/// Creates a custom tokenizer.
///
/// The arguments parameter is an array of String built from the CREATE
/// VIRTUAL TABLE statement. In the example below, the arguments will
/// be `["arg1", "arg2"]`.
///
/// CREATE VIRTUAL TABLE document USING fts5(
/// tokenize='custom arg1 arg2'
/// )
///
/// - parameter db: A Database connection
/// - parameter arguments: An array of string arguments
init(db: Database, arguments: [String]) throws
}
extension FTS5CustomTokenizer {
/// Creates an FTS5 tokenizer descriptor.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// let tokenizer = MyTokenizer.tokenizerDescriptor(arguments: ["unicode61", "remove_diacritics", "0"])
/// t.tokenizer = tokenizer
/// }
public static func tokenizerDescriptor(arguments: [String] = []) -> FTS5TokenizerDescriptor {
return FTS5TokenizerDescriptor(components: [name] + arguments)
}
extension Database {
}
extension Database {
// MARK: - FTS5
private class FTS5TokenizerConstructor {
let db: Database
let constructor: (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32
// MARK: - FTS5
private class FTS5TokenizerConstructor {
let db: Database
let constructor: (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32
init(db: Database, constructor: @escaping (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32) {
self.db = db
self.constructor = constructor
}
}
/// Add a custom FTS5 tokenizer.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
/// db.add(tokenizer: MyTokenizer.self)
public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) {
let api = FTS5.api(self)
// Swift won't let the @convention(c) xCreate() function below create
// an instance of the generic Tokenizer type.
//
// We thus hide the generic Tokenizer type inside a neutral type:
// FTS5TokenizerConstructor
let constructor = FTS5TokenizerConstructor(
db: self,
constructor: { (db, arguments, tokenizerHandle) in
guard let tokenizerHandle = tokenizerHandle else {
return SQLITE_ERROR
}
do {
let tokenizer = try Tokenizer(db: db, arguments: arguments)
// Tokenizer must remain alive until xDeleteTokenizer()
// is called, as the xDelete member of xTokenizer
let tokenizerPointer = OpaquePointer(Unmanaged.passRetained(tokenizer).toOpaque())
tokenizerHandle.pointee = tokenizerPointer
return SQLITE_OK
} catch let error as DatabaseError {
return error.extendedResultCode.rawValue
} catch {
return SQLITE_ERROR
}
})
// Constructor must remain alive until deleteConstructor() is
// called, as the last argument of the xCreateTokenizer() function.
let constructorPointer = Unmanaged.passRetained(constructor).toOpaque()
func deleteConstructor(constructorPointer: UnsafeMutableRawPointer?) {
guard let constructorPointer = constructorPointer else { return }
Unmanaged<AnyObject>.fromOpaque(constructorPointer).release()
}
func xCreateTokenizer(constructorPointer: UnsafeMutableRawPointer?, azArg: UnsafeMutablePointer<UnsafePointer<Int8>?>?, nArg: Int32, tokenizerHandle: UnsafeMutablePointer<OpaquePointer?>?) -> Int32 {
guard let constructorPointer = constructorPointer else {
return SQLITE_ERROR
}
let constructor = Unmanaged<FTS5TokenizerConstructor>.fromOpaque(constructorPointer).takeUnretainedValue()
var arguments: [String] = []
if let azArg = azArg {
for i in 0..<Int(nArg) {
if let cstr = azArg[i] {
arguments.append(String(cString: cstr))
}
}
}
return constructor.constructor(constructor.db, arguments, tokenizerHandle)
}
func xDeleteTokenizer(tokenizerPointer: OpaquePointer?) {
guard let tokenizerPointer = tokenizerPointer else { return }
Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).release()
}
func xTokenize(tokenizerPointer: OpaquePointer?, context: UnsafeMutableRawPointer?, flags: Int32, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafePointer<Int8>?, Int32, Int32, Int32) -> Int32)?) -> Int32 {
guard let tokenizerPointer = tokenizerPointer else {
return SQLITE_ERROR
}
let object = Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).takeUnretainedValue()
guard let tokenizer = object as? FTS5Tokenizer else {
return SQLITE_ERROR
}
return tokenizer.tokenize(context: context, tokenization: FTS5Tokenization(rawValue: flags), pText: pText, nText: nText, tokenCallback: tokenCallback!)
}
var xTokenizer = fts5_tokenizer(xCreate: xCreateTokenizer, xDelete: xDeleteTokenizer, xTokenize: xTokenize)
let code = withUnsafeMutablePointer(to: &xTokenizer) { xTokenizerPointer in
api.pointee.xCreateTokenizer(UnsafeMutablePointer(mutating: api), Tokenizer.name, constructorPointer, xTokenizerPointer, deleteConstructor)
}
guard code == SQLITE_OK else {
// Assume a GRDB bug: there is no point throwing any error.
fatalError(DatabaseError(resultCode: code, message: lastErrorMessage).description)
}
init(db: Database, constructor: @escaping (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32) {
self.db = db
self.constructor = constructor
}
}
extension DatabaseQueue {
/// Add a custom FTS5 tokenizer.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
/// db.add(tokenizer: MyTokenizer.self)
public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) {
let api = FTS5.api(self)
// MARK: - Custom FTS5 Tokenizers
// Swift won't let the @convention(c) xCreate() function below create
// an instance of the generic Tokenizer type.
//
// We thus hide the generic Tokenizer type inside a neutral type:
// FTS5TokenizerConstructor
let constructor = FTS5TokenizerConstructor(
db: self,
constructor: { (db, arguments, tokenizerHandle) in
guard let tokenizerHandle = tokenizerHandle else {
return SQLITE_ERROR
}
do {
let tokenizer = try Tokenizer(db: db, arguments: arguments)
// Tokenizer must remain alive until xDeleteTokenizer()
// is called, as the xDelete member of xTokenizer
let tokenizerPointer = OpaquePointer(Unmanaged.passRetained(tokenizer).toOpaque())
tokenizerHandle.pointee = tokenizerPointer
return SQLITE_OK
} catch let error as DatabaseError {
return error.extendedResultCode.rawValue
} catch {
return SQLITE_ERROR
}
})
/// Add a custom FTS5 tokenizer.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
/// dbQueue.add(tokenizer: MyTokenizer.self)
public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) {
inDatabase { db in
db.add(tokenizer: Tokenizer.self)
// Constructor must remain alive until deleteConstructor() is
// called, as the last argument of the xCreateTokenizer() function.
let constructorPointer = Unmanaged.passRetained(constructor).toOpaque()
func deleteConstructor(constructorPointer: UnsafeMutableRawPointer?) {
guard let constructorPointer = constructorPointer else { return }
Unmanaged<AnyObject>.fromOpaque(constructorPointer).release()
}
func xCreateTokenizer(constructorPointer: UnsafeMutableRawPointer?, azArg: UnsafeMutablePointer<UnsafePointer<Int8>?>?, nArg: Int32, tokenizerHandle: UnsafeMutablePointer<OpaquePointer?>?) -> Int32 {
guard let constructorPointer = constructorPointer else {
return SQLITE_ERROR
}
let constructor = Unmanaged<FTS5TokenizerConstructor>.fromOpaque(constructorPointer).takeUnretainedValue()
var arguments: [String] = []
if let azArg = azArg {
for i in 0..<Int(nArg) {
if let cstr = azArg[i] {
arguments.append(String(cString: cstr))
}
}
}
return constructor.constructor(constructor.db, arguments, tokenizerHandle)
}
func xDeleteTokenizer(tokenizerPointer: OpaquePointer?) {
guard let tokenizerPointer = tokenizerPointer else { return }
Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).release()
}
func xTokenize(tokenizerPointer: OpaquePointer?, context: UnsafeMutableRawPointer?, flags: Int32, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafePointer<Int8>?, Int32, Int32, Int32) -> Int32)?) -> Int32 {
guard let tokenizerPointer = tokenizerPointer else {
return SQLITE_ERROR
}
let object = Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).takeUnretainedValue()
guard let tokenizer = object as? FTS5Tokenizer else {
return SQLITE_ERROR
}
return tokenizer.tokenize(context: context, tokenization: FTS5Tokenization(rawValue: flags), pText: pText, nText: nText, tokenCallback: tokenCallback!)
}
var xTokenizer = fts5_tokenizer(xCreate: xCreateTokenizer, xDelete: xDeleteTokenizer, xTokenize: xTokenize)
let code = withUnsafeMutablePointer(to: &xTokenizer) { xTokenizerPointer in
api.pointee.xCreateTokenizer(UnsafeMutablePointer(mutating: api), Tokenizer.name, constructorPointer, xTokenizerPointer, deleteConstructor)
}
guard code == SQLITE_OK else {
// Assume a GRDB bug: there is no point throwing any error.
fatalError(DatabaseError(resultCode: code, message: lastErrorMessage).description)
}
}
}
extension DatabaseQueue {
// MARK: - Custom FTS5 Tokenizers
/// Add a custom FTS5 tokenizer.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
/// dbQueue.add(tokenizer: MyTokenizer.self)
public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) {
inDatabase { db in
db.add(tokenizer: Tokenizer.self)
}
}
}
#endif

View File

@ -1,121 +1,121 @@
#if SQLITE_ENABLE_FTS5
/// A full text pattern that can query FTS5 virtual tables.
public struct FTS5Pattern {
/// The raw pattern string. Guaranteed to be a valid FTS5 pattern.
public let rawPattern: String
/// Creates a pattern that matches any token found in the input string;
/// returns nil if no pattern could be built.
///
/// FTS5Pattern(matchingAnyTokenIn: "") // nil
/// FTS5Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingAnyTokenIn string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: tokens.joined(separator: " OR "))
}
/// Creates a pattern that matches all tokens found in the input string;
/// returns nil if no pattern could be built.
///
/// FTS5Pattern(matchingAllTokensIn: "") // nil
/// FTS5Pattern(matchingAllTokensIn: "foo bar") // foo bar
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingAllTokensIn string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: tokens.joined(separator: " "))
}
/// Creates a pattern that matches a contiguous string; returns nil if no
/// pattern could be built.
///
/// FTS5Pattern(matchingPhrase: "") // nil
/// FTS5Pattern(matchingPhrase: "foo bar") // "foo bar"
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingPhrase string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: "\"" + tokens.joined(separator: " ") + "\"")
}
/// Creates a pattern that matches a contiguous string prefix; returns
/// nil if no pattern could be built.
///
/// FTS5Pattern(matchingPrefixPhrase: "") // nil
/// FTS5Pattern(matchingPrefixPhrase: "foo bar") // ^"foo bar"
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingPrefixPhrase string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: "^\"" + tokens.joined(separator: " ") + "\"")
}
init(rawPattern: String, allowedColumns: [String] = []) throws {
// Correctness above all: use SQLite to validate the pattern.
//
// Invalid patterns have SQLite return an error on the first
// call to sqlite3_step() on a statement that matches against
// that pattern.
do {
try DatabaseQueue().inDatabase { db in
try db.create(virtualTable: "document", using: FTS5()) { t in
if allowedColumns.isEmpty {
t.column("__grdb__")
} else {
for column in allowedColumns {
t.column(column)
}
/// A full text pattern that can query FTS5 virtual tables.
public struct FTS5Pattern {
/// The raw pattern string. Guaranteed to be a valid FTS5 pattern.
public let rawPattern: String
/// Creates a pattern that matches any token found in the input string;
/// returns nil if no pattern could be built.
///
/// FTS5Pattern(matchingAnyTokenIn: "") // nil
/// FTS5Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingAnyTokenIn string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: tokens.joined(separator: " OR "))
}
/// Creates a pattern that matches all tokens found in the input string;
/// returns nil if no pattern could be built.
///
/// FTS5Pattern(matchingAllTokensIn: "") // nil
/// FTS5Pattern(matchingAllTokensIn: "foo bar") // foo bar
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingAllTokensIn string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: tokens.joined(separator: " "))
}
/// Creates a pattern that matches a contiguous string; returns nil if no
/// pattern could be built.
///
/// FTS5Pattern(matchingPhrase: "") // nil
/// FTS5Pattern(matchingPhrase: "foo bar") // "foo bar"
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingPhrase string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: "\"" + tokens.joined(separator: " ") + "\"")
}
/// Creates a pattern that matches a contiguous string prefix; returns
/// nil if no pattern could be built.
///
/// FTS5Pattern(matchingPrefixPhrase: "") // nil
/// FTS5Pattern(matchingPrefixPhrase: "foo bar") // ^"foo bar"
///
/// - parameter string: The string to turn into an FTS5 pattern
public init?(matchingPrefixPhrase string: String) {
guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil }
guard !tokens.isEmpty else { return nil }
try? self.init(rawPattern: "^\"" + tokens.joined(separator: " ") + "\"")
}
init(rawPattern: String, allowedColumns: [String] = []) throws {
// Correctness above all: use SQLite to validate the pattern.
//
// Invalid patterns have SQLite return an error on the first
// call to sqlite3_step() on a statement that matches against
// that pattern.
do {
try DatabaseQueue().inDatabase { db in
try db.create(virtualTable: "document", using: FTS5()) { t in
if allowedColumns.isEmpty {
t.column("__grdb__")
} else {
for column in allowedColumns {
t.column(column)
}
}
try db.makeSelectStatement("SELECT * FROM document WHERE document MATCH ?")
.makeCursor(arguments: [rawPattern])
.next() // error on next() for invalid patterns
}
} catch let error as DatabaseError {
// Remove private SQL & arguments from the thrown error
throw DatabaseError(resultCode: error.extendedResultCode, message: error.message, sql: nil, arguments: nil)
try db.makeSelectStatement(sql: "SELECT * FROM document WHERE document MATCH ?")
.makeCursor(arguments: [rawPattern])
.next() // error on next() for invalid patterns
}
// Pattern is valid
self.rawPattern = rawPattern
} catch let error as DatabaseError {
// Remove private SQL & arguments from the thrown error
throw DatabaseError(resultCode: error.extendedResultCode, message: error.message, sql: nil, arguments: nil)
}
// Pattern is valid
self.rawPattern = rawPattern
}
}
extension Database {
// MARK: - FTS5
/// Creates a pattern from a raw pattern string; throws DatabaseError on
/// invalid syntax.
///
/// The pattern syntax is documented at https://www.sqlite.org/fts5.html#full_text_query_syntax
///
/// try db.makeFTS5Pattern(rawPattern: "and", forTable: "document") // OK
/// try db.makeFTS5Pattern(rawPattern: "AND", forTable: "document") // malformed MATCH expression: [AND]
public func makeFTS5Pattern(rawPattern: String, forTable table: String) throws -> FTS5Pattern {
return try FTS5Pattern(rawPattern: rawPattern, allowedColumns: columns(in: table).map { $0.name })
}
extension Database {
// MARK: - FTS5
/// Creates a pattern from a raw pattern string; throws DatabaseError on
/// invalid syntax.
///
/// The pattern syntax is documented at https://www.sqlite.org/fts5.html#full_text_query_syntax
///
/// try db.makeFTS5Pattern(rawPattern: "and", forTable: "document") // OK
/// try db.makeFTS5Pattern(rawPattern: "AND", forTable: "document") // malformed MATCH expression: [AND]
public func makeFTS5Pattern(rawPattern: String, forTable table: String) throws -> FTS5Pattern {
return try FTS5Pattern(rawPattern: rawPattern, allowedColumns: columns(in: table).map { $0.name })
}
}
extension FTS5Pattern : DatabaseValueConvertible {
/// Returns a value that can be stored in the database.
public var databaseValue: DatabaseValue {
return rawPattern.databaseValue
}
/// Returns an FTS5Pattern initialized from *dbValue*, if it
/// contains a suitable value.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> FTS5Pattern? {
return String
.fromDatabaseValue(dbValue)
.flatMap { try? FTS5Pattern(rawPattern: $0) }
}
extension FTS5Pattern : DatabaseValueConvertible {
/// Returns a value that can be stored in the database.
public var databaseValue: DatabaseValue {
return rawPattern.databaseValue
}
/// Returns an FTS5Pattern initialized from *dbValue*, if it
/// contains a suitable value.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> FTS5Pattern? {
return String
.fromDatabaseValue(dbValue)
.flatMap { try? FTS5Pattern(rawPattern: $0) }
}
}
#endif

View File

@ -1,214 +1,214 @@
#if SQLITE_ENABLE_FTS5
/// A low-level SQLite function that lets FTS5Tokenizer notify tokens.
///
/// See FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:)
public typealias FTS5TokenCallback = @convention(c) (_ context: UnsafeMutableRawPointer?, _ flags: Int32, _ pToken: UnsafePointer<Int8>?, _ nToken: Int32, _ iStart: Int32, _ iEnd: Int32) -> Int32
/// The reason why FTS5 is requesting tokenization.
///
/// See https://www.sqlite.org/fts5.html#custom_tokenizers
public struct FTS5Tokenization : OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
/// FTS5_TOKENIZE_QUERY
public static let query = FTS5Tokenization(rawValue: FTS5_TOKENIZE_QUERY)
/// FTS5_TOKENIZE_PREFIX
public static let prefix = FTS5Tokenization(rawValue: FTS5_TOKENIZE_PREFIX)
/// FTS5_TOKENIZE_DOCUMENT
public static let document = FTS5Tokenization(rawValue: FTS5_TOKENIZE_DOCUMENT)
/// FTS5_TOKENIZE_AUX
public static let aux = FTS5Tokenization(rawValue: FTS5_TOKENIZE_AUX)
}
/// The protocol for FTS5 tokenizers
public protocol FTS5Tokenizer : class {
/// Tokenizes the text described by `pText` and `nText`, and
/// notifies found tokens to the `tokenCallback` function.
///
/// It matches the `xTokenize` function documented at https://www.sqlite.org/fts5.html#custom_tokenizers
///
/// - parameters:
/// - context: An opaque pointer that is the first argument to
/// the `tokenCallback` function
/// - tokenization: The reason why FTS5 is requesting tokenization.
/// - pText: The tokenized text bytes. May or may not be
/// nul-terminated.
/// - nText: The number of bytes in the tokenized text.
/// - tokenCallback: The function to call for each found token.
/// It matches the `xToken` callback at https://www.sqlite.org/fts5.html#custom_tokenizers
func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32
}
private class TokenizeContext {
var tokens: [(String, FTS5TokenFlags)] = []
}
extension FTS5Tokenizer {
/// Tokenizes the string argument into an array of
/// (String, FTS5TokenFlags) pairs.
///
/// let tokenizer = try db.makeTokenizer(.ascii())
/// try tokenizer.tokenize("foo bar", for: .document) // [("foo", flags), ("bar", flags)]
///
/// - parameter string: The string to tokenize
/// - parameter tokenization: The reason why tokenization is requested:
/// - .document: Tokenize like a document being inserted into an FTS table.
/// - .query: Tokenize like the search pattern of the MATCH operator.
/// - parameter tokenizer: A FTS5TokenizerDescriptor such as .ascii()
func tokenize(_ string: String, for tokenization: FTS5Tokenization) throws -> [(String, FTS5TokenFlags)] {
return try ContiguousArray(string.utf8).withUnsafeBufferPointer { buffer -> [(String, FTS5TokenFlags)] in
guard let addr = buffer.baseAddress else {
return []
}
let pText = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self)
let nText = Int32(buffer.count)
var context = TokenizeContext()
try withUnsafeMutablePointer(to: &context) { contextPointer in
let code = tokenize(context: UnsafeMutableRawPointer(contextPointer), tokenization: tokenization, pText: pText, nText: nText, tokenCallback: { (contextPointer, flags, pToken, nToken, iStart, iEnd) -> Int32 in
guard let contextPointer = contextPointer else { return SQLITE_ERROR }
// Extract token
guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else {
return SQLITE_OK
}
let context = contextPointer.assumingMemoryBound(to: TokenizeContext.self).pointee
context.tokens.append((token, FTS5TokenFlags(rawValue: flags)))
return SQLITE_OK
})
if (code != SQLITE_OK) {
throw DatabaseError(resultCode: code)
}
}
return context.tokens
}
}
func nonSynonymTokens(in string: String, for tokenization: FTS5Tokenization) throws -> [String] {
var tokens: [String] = []
for (token, flags) in try tokenize(string, for: tokenization) {
if !flags.contains(.colocated) {
tokens.append(token)
}
}
return tokens
}
}
extension Database {
// MARK: - FTS5
/// A low-level SQLite function that lets FTS5Tokenizer notify tokens.
///
/// See FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:)
public typealias FTS5TokenCallback = @convention(c) (_ context: UnsafeMutableRawPointer?, _ flags: Int32, _ pToken: UnsafePointer<Int8>?, _ nToken: Int32, _ iStart: Int32, _ iEnd: Int32) -> Int32
/// Private type that makes a pre-registered FTS5 tokenizer available
/// through the FTS5Tokenizer protocol.
private final class FTS5RegisteredTokenizer : FTS5Tokenizer {
let xTokenizer: fts5_tokenizer
let tokenizerPointer: OpaquePointer
/// The reason why FTS5 is requesting tokenization.
///
/// See https://www.sqlite.org/fts5.html#custom_tokenizers
public struct FTS5Tokenization : OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
/// FTS5_TOKENIZE_QUERY
public static let query = FTS5Tokenization(rawValue: FTS5_TOKENIZE_QUERY)
/// FTS5_TOKENIZE_PREFIX
public static let prefix = FTS5Tokenization(rawValue: FTS5_TOKENIZE_PREFIX)
/// FTS5_TOKENIZE_DOCUMENT
public static let document = FTS5Tokenization(rawValue: FTS5_TOKENIZE_DOCUMENT)
/// FTS5_TOKENIZE_AUX
public static let aux = FTS5Tokenization(rawValue: FTS5_TOKENIZE_AUX)
}
/// The protocol for FTS5 tokenizers
public protocol FTS5Tokenizer : class {
/// Tokenizes the text described by `pText` and `nText`, and
/// notifies found tokens to the `tokenCallback` function.
///
/// It matches the `xTokenize` function documented at https://www.sqlite.org/fts5.html#custom_tokenizers
///
/// - parameters:
/// - context: An opaque pointer that is the first argument to
/// the `tokenCallback` function
/// - tokenization: The reason why FTS5 is requesting tokenization.
/// - pText: The tokenized text bytes. May or may not be
/// nul-terminated.
/// - nText: The number of bytes in the tokenized text.
/// - tokenCallback: The function to call for each found token.
/// It matches the `xToken` callback at https://www.sqlite.org/fts5.html#custom_tokenizers
func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32
}
private class TokenizeContext {
var tokens: [(String, FTS5TokenFlags)] = []
}
extension FTS5Tokenizer {
/// Tokenizes the string argument into an array of
/// (String, FTS5TokenFlags) pairs.
///
/// let tokenizer = try db.makeTokenizer(.ascii())
/// try tokenizer.tokenize("foo bar", for: .document) // [("foo", flags), ("bar", flags)]
///
/// - parameter string: The string to tokenize
/// - parameter tokenization: The reason why tokenization is requested:
/// - .document: Tokenize like a document being inserted into an FTS table.
/// - .query: Tokenize like the search pattern of the MATCH operator.
/// - parameter tokenizer: A FTS5TokenizerDescriptor such as .ascii()
func tokenize(_ string: String, for tokenization: FTS5Tokenization) throws -> [(String, FTS5TokenFlags)] {
return try ContiguousArray(string.utf8).withUnsafeBufferPointer { buffer -> [(String, FTS5TokenFlags)] in
guard let addr = buffer.baseAddress else {
return []
}
let pText = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self)
let nText = Int32(buffer.count)
init(xTokenizer: fts5_tokenizer, contextPointer: UnsafeMutableRawPointer?, arguments: [String]) throws {
guard let xCreate = xTokenizer.xCreate else {
throw DatabaseError(resultCode: .SQLITE_ERROR, message: "nil fts5_tokenizer.xCreate")
}
self.xTokenizer = xTokenizer
var tokenizerPointer: OpaquePointer? = nil
let code: Int32
if let argument = arguments.first {
// Turn [String] into ContiguousArray<UnsafePointer<Int8>>
// (for an alternative implementation see https://oleb.net/blog/2016/10/swift-array-of-c-strings/)
func convertArguments<Result>(_ array: inout ContiguousArray<UnsafePointer<Int8>>, _ car: String, _ cdr: [String], _ body: (ContiguousArray<UnsafePointer<Int8>>) -> Result) -> Result {
return car.withCString { cString in
if let car = cdr.first {
array.append(cString)
return convertArguments(&array, car, Array(cdr.suffix(from: 1)), body)
} else {
return body(array)
}
}
var context = TokenizeContext()
try withUnsafeMutablePointer(to: &context) { contextPointer in
let code = tokenize(context: UnsafeMutableRawPointer(contextPointer), tokenization: tokenization, pText: pText, nText: nText, tokenCallback: { (contextPointer, flags, pToken, nToken, iStart, iEnd) -> Int32 in
guard let contextPointer = contextPointer else { return SQLITE_ERROR }
// Extract token
guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else {
return SQLITE_OK
}
var cStrings = ContiguousArray<UnsafePointer<Int8>>()
code = convertArguments(&cStrings, argument, Array(arguments.suffix(from: 1))) { cStrings in
cStrings.withUnsafeBufferPointer { azArg in
xCreate(contextPointer, UnsafeMutablePointer(OpaquePointer(azArg.baseAddress!)), Int32(cStrings.count), &tokenizerPointer)
}
}
} else {
code = xCreate(contextPointer, nil, 0, &tokenizerPointer)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: "failed fts5_tokenizer.xCreate")
}
if let tokenizerPointer = tokenizerPointer {
self.tokenizerPointer = tokenizerPointer
} else {
throw DatabaseError(resultCode: code, message: "nil tokenizer")
let context = contextPointer.assumingMemoryBound(to: TokenizeContext.self).pointee
context.tokens.append((token, FTS5TokenFlags(rawValue: flags)))
return SQLITE_OK
})
if (code != SQLITE_OK) {
throw DatabaseError(resultCode: code)
}
}
deinit {
if let delete = xTokenizer.xDelete {
delete(tokenizerPointer)
}
}
func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 {
guard let xTokenize = xTokenizer.xTokenize else {
return SQLITE_ERROR
}
return xTokenize(tokenizerPointer, context, tokenization.rawValue, pText, nText, tokenCallback)
return context.tokens
}
}
func nonSynonymTokens(in string: String, for tokenization: FTS5Tokenization) throws -> [String] {
var tokens: [String] = []
for (token, flags) in try tokenize(string, for: tokenization) {
if !flags.contains(.colocated) {
tokens.append(token)
}
}
return tokens
}
}
extension Database {
// MARK: - FTS5
/// Private type that makes a pre-registered FTS5 tokenizer available
/// through the FTS5Tokenizer protocol.
private final class FTS5RegisteredTokenizer : FTS5Tokenizer {
let xTokenizer: fts5_tokenizer
let tokenizerPointer: OpaquePointer
/// Creates an FTS5 tokenizer, given its descriptor.
///
/// let unicode61 = try db.makeTokenizer(.unicode61())
///
/// It is a programmer error to use the tokenizer outside of a protected
/// database queue, or after the database has been closed.
///
/// Use this method when you implement a custom wrapper tokenizer:
///
/// final class MyTokenizer : FTS5WrapperTokenizer {
/// var wrappedTokenizer: FTS5Tokenizer
///
/// init(db: Database, arguments: [String]) throws {
/// wrappedTokenizer = try db.makeTokenizer(.unicode61())
/// }
/// }
public func makeTokenizer(_ descriptor: FTS5TokenizerDescriptor) throws -> FTS5Tokenizer {
let api = FTS5.api(self)
init(xTokenizer: fts5_tokenizer, contextPointer: UnsafeMutableRawPointer?, arguments: [String]) throws {
guard let xCreate = xTokenizer.xCreate else {
throw DatabaseError(resultCode: .SQLITE_ERROR, message: "nil fts5_tokenizer.xCreate")
}
let xTokenizerPointer: UnsafeMutablePointer<fts5_tokenizer> = .allocate(capacity: 1)
defer { xTokenizerPointer.deallocate() }
self.xTokenizer = xTokenizer
let contextHandle: UnsafeMutablePointer<UnsafeMutableRawPointer?> = .allocate(capacity: 1)
defer { contextHandle.deallocate() }
let code = api.pointee.xFindTokenizer!(
UnsafeMutablePointer(mutating: api),
descriptor.name,
contextHandle,
xTokenizerPointer)
var tokenizerPointer: OpaquePointer? = nil
let code: Int32
if let argument = arguments.first {
// Turn [String] into ContiguousArray<UnsafePointer<Int8>>
// (for an alternative implementation see https://oleb.net/blog/2016/10/swift-array-of-c-strings/)
func convertArguments<Result>(_ array: inout ContiguousArray<UnsafePointer<Int8>>, _ car: String, _ cdr: [String], _ body: (ContiguousArray<UnsafePointer<Int8>>) -> Result) -> Result {
return car.withCString { cString in
if let car = cdr.first {
array.append(cString)
return convertArguments(&array, car, Array(cdr.suffix(from: 1)), body)
} else {
return body(array)
}
}
}
var cStrings = ContiguousArray<UnsafePointer<Int8>>()
code = convertArguments(&cStrings, argument, Array(arguments.suffix(from: 1))) { cStrings in
cStrings.withUnsafeBufferPointer { azArg in
xCreate(contextPointer, UnsafeMutablePointer(OpaquePointer(azArg.baseAddress!)), Int32(cStrings.count), &tokenizerPointer)
}
}
} else {
code = xCreate(contextPointer, nil, 0, &tokenizerPointer)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code)
throw DatabaseError(resultCode: code, message: "failed fts5_tokenizer.xCreate")
}
let contextPointer = contextHandle.pointee
return try FTS5RegisteredTokenizer(xTokenizer: xTokenizerPointer.pointee, contextPointer: contextPointer, arguments: descriptor.arguments)
if let tokenizerPointer = tokenizerPointer {
self.tokenizerPointer = tokenizerPointer
} else {
throw DatabaseError(resultCode: code, message: "nil tokenizer")
}
}
deinit {
if let delete = xTokenizer.xDelete {
delete(tokenizerPointer)
}
}
func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 {
guard let xTokenize = xTokenizer.xTokenize else {
return SQLITE_ERROR
}
return xTokenize(tokenizerPointer, context, tokenization.rawValue, pText, nText, tokenCallback)
}
}
/// Creates an FTS5 tokenizer, given its descriptor.
///
/// let unicode61 = try db.makeTokenizer(.unicode61())
///
/// It is a programmer error to use the tokenizer outside of a protected
/// database queue, or after the database has been closed.
///
/// Use this method when you implement a custom wrapper tokenizer:
///
/// final class MyTokenizer : FTS5WrapperTokenizer {
/// var wrappedTokenizer: FTS5Tokenizer
///
/// init(db: Database, arguments: [String]) throws {
/// wrappedTokenizer = try db.makeTokenizer(.unicode61())
/// }
/// }
public func makeTokenizer(_ descriptor: FTS5TokenizerDescriptor) throws -> FTS5Tokenizer {
let api = FTS5.api(self)
let xTokenizerPointer: UnsafeMutablePointer<fts5_tokenizer> = .allocate(capacity: 1)
defer { xTokenizerPointer.deallocate() }
let contextHandle: UnsafeMutablePointer<UnsafeMutableRawPointer?> = .allocate(capacity: 1)
defer { contextHandle.deallocate() }
let code = api.pointee.xFindTokenizer!(
UnsafeMutablePointer(mutating: api),
descriptor.name,
contextHandle,
xTokenizerPointer)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code)
}
let contextPointer = contextHandle.pointee
return try FTS5RegisteredTokenizer(xTokenizer: xTokenizerPointer.pointee, contextPointer: contextPointer, arguments: descriptor.arguments)
}
}
#endif

View File

@ -1,117 +1,138 @@
#if SQLITE_ENABLE_FTS5
/// An FTS5 tokenizer, suitable for FTS5 table definitions:
/// An FTS5 tokenizer, suitable for FTS5 table definitions:
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .unicode61() // FTS5TokenizerDescriptor
/// }
///
/// See https://www.sqlite.org/fts5.html#tokenizers
public struct FTS5TokenizerDescriptor {
/// The tokenizer components
///
/// // ["unicode61"]
/// FTS5TokenizerDescriptor.unicode61().components
///
/// // ["unicode61", "remove_diacritics", "0"]
/// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).components
public let components: [String]
/// The tokenizer name
///
/// // "unicode61"
/// FTS5TokenizerDescriptor.unicode61().name
///
/// // "unicode61"
/// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).name
var name: String {
return components[0]
}
var arguments: [String] {
return Array(components.suffix(from: 1))
}
/// Creates an FTS5 tokenizer descriptor.
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .unicode61() // FTS5TokenizerDescriptor
/// let tokenizer = FTS5TokenizerDescriptor(components: ["porter", "unicode61", "remove_diacritics", "0"])
/// t.tokenizer = tokenizer
/// }
///
/// See https://www.sqlite.org/fts5.html#tokenizers
public struct FTS5TokenizerDescriptor {
/// The tokenizer components
///
/// // ["unicode61"]
/// FTS5TokenizerDescriptor.unicode61().components
///
/// // ["unicode61", "remove_diacritics", "0"]
/// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).components
public let components: [String]
/// The tokenizer name
///
/// // "unicode61"
/// FTS5TokenizerDescriptor.unicode61().name
///
/// // "unicode61"
/// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).name
var name: String {
return components[0]
}
var arguments: [String] {
return Array(components.suffix(from: 1))
}
/// Creates an FTS5 tokenizer descriptor.
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// let tokenizer = FTS5TokenizerDescriptor(components: ["porter", "unicode61", "remove_diacritics", "0"])
/// t.tokenizer = tokenizer
/// }
///
/// - precondition: Components is not empty
public init(components: [String]) {
GRDBPrecondition(!components.isEmpty, "FTS5TokenizerDescriptor requires at least one component")
assert(!components.isEmpty)
self.components = components
}
/// The "ascii" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .ascii()
/// }
///
/// - parameters:
/// - separators: Unless empty (the default), SQLite will consider
/// these characters as token separators.
///
/// See https://www.sqlite.org/fts5.html#ascii_tokenizer
public static func ascii(separators: Set<Character> = []) -> FTS5TokenizerDescriptor {
if separators.isEmpty {
return FTS5TokenizerDescriptor(components: ["ascii"])
} else {
return FTS5TokenizerDescriptor(components: ["ascii", "separators", separators.map { String($0) }.joined(separator: "").sqlExpression.sql])
}
}
/// The "porter" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .porter()
/// }
///
/// - parameters:
/// - base: An eventual wrapping tokenizer which replaces the
// default unicode61() base tokenizer.
///
/// See https://www.sqlite.org/fts5.html#porter_tokenizer
public static func porter(wrapping base: FTS5TokenizerDescriptor? = nil) -> FTS5TokenizerDescriptor {
if let base = base {
return FTS5TokenizerDescriptor(components: ["porter"] + base.components)
} else {
return FTS5TokenizerDescriptor(components: ["porter"])
}
}
/// An "unicode61" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .unicode61()
/// }
///
/// - parameters:
/// - removeDiacritics: If true (the default), then SQLite will
/// strip diacritics from latin characters.
/// - separators: Unless empty (the default), SQLite will consider
/// these characters as token separators.
/// - tokenCharacters: Unless empty (the default), SQLite will
/// consider these characters as token characters.
///
/// See https://www.sqlite.org/fts5.html#unicode61_tokenizer
public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS5TokenizerDescriptor {
var components: [String] = ["unicode61"]
if !removeDiacritics {
components.append(contentsOf: ["remove_diacritics", "0"])
}
if !separators.isEmpty {
// TODO: test "=" and "\"", "(" and ")" as separators, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:)
components.append(contentsOf: ["separators", separators.sorted().map { String($0) }.joined(separator: "").sqlExpression.sql])
}
if !tokenCharacters.isEmpty {
// TODO: test "=" and "\"", "(" and ")" as tokenCharacters, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:)
components.append(contentsOf: ["tokenchars", tokenCharacters.sorted().map { String($0) }.joined(separator: "").sqlExpression.sql])
}
return FTS5TokenizerDescriptor(components: components)
/// - precondition: Components is not empty
public init(components: [String]) {
GRDBPrecondition(!components.isEmpty, "FTS5TokenizerDescriptor requires at least one component")
assert(!components.isEmpty)
self.components = components
}
/// The "ascii" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .ascii()
/// }
///
/// - parameters:
/// - separators: Unless empty (the default), SQLite will consider
/// these characters as token separators.
///
/// See https://www.sqlite.org/fts5.html#ascii_tokenizer
public static func ascii(separators: Set<Character> = []) -> FTS5TokenizerDescriptor {
if separators.isEmpty {
return FTS5TokenizerDescriptor(components: ["ascii"])
} else {
return FTS5TokenizerDescriptor(components: ["ascii", "separators", separators.map { String($0) }.joined(separator: "").sqlExpression.quotedSQL()])
}
}
/// The "porter" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .porter()
/// }
///
/// - parameters:
/// - base: An eventual wrapping tokenizer which replaces the
// default unicode61() base tokenizer.
///
/// See https://www.sqlite.org/fts5.html#porter_tokenizer
public static func porter(wrapping base: FTS5TokenizerDescriptor? = nil) -> FTS5TokenizerDescriptor {
if let base = base {
return FTS5TokenizerDescriptor(components: ["porter"] + base.components)
} else {
return FTS5TokenizerDescriptor(components: ["porter"])
}
}
/// An "unicode61" tokenizer
///
/// db.create(virtualTable: "book", using: FTS5()) { t in
/// t.tokenizer = .unicode61()
/// }
///
/// - parameters:
/// - diacritics: By default SQLite will strip diacritics from
/// latin characters.
/// - separators: Unless empty (the default), SQLite will consider
/// these characters as token separators.
/// - tokenCharacters: Unless empty (the default), SQLite will
/// consider these characters as token characters.
///
/// See https://www.sqlite.org/fts5.html#unicode61_tokenizer
public static func unicode61(diacritics: FTS5.Diacritics = .removeLegacy, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS5TokenizerDescriptor {
var components: [String] = ["unicode61"]
switch diacritics {
case .removeLegacy:
break
case .keep:
components.append(contentsOf: ["remove_diacritics", "0"])
#if GRDBCUSTOMSQLITE
case .remove:
components.append(contentsOf: ["remove_diacritics", "2"])
#endif
}
if !separators.isEmpty {
// TODO: test "=" and "\"", "(" and ")" as separators, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:)
components.append(contentsOf: [
"separators",
separators
.sorted()
.map { String($0) }
.joined(separator: "")
.sqlExpression
.quotedSQL()])
}
if !tokenCharacters.isEmpty {
// TODO: test "=" and "\"", "(" and ")" as tokenCharacters, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:)
components.append(contentsOf: [
"tokenchars",
tokenCharacters
.sorted()
.map { String($0) }
.joined(separator: "")
.sqlExpression
.quotedSQL()])
}
return FTS5TokenizerDescriptor(components: components)
}
}
#endif

View File

@ -1,141 +1,141 @@
#if SQLITE_ENABLE_FTS5
/// Flags that tell SQLite how to register a token.
///
/// See https://www.sqlite.org/fts5.html#custom_tokenizers
public struct FTS5TokenFlags : OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
/// FTS5_TOKEN_COLOCATED
public static let colocated = FTS5TokenFlags(rawValue: FTS5_TOKEN_COLOCATED)
/// Flags that tell SQLite how to register a token.
///
/// See https://www.sqlite.org/fts5.html#custom_tokenizers
public struct FTS5TokenFlags : OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
/// A function that lets FTS5WrapperTokenizer notify tokens.
///
/// See FTS5WrapperTokenizer.accept(token:flags:tokenCallback:)
public typealias FTS5WrapperTokenCallback = (_ token: String, _ flags: FTS5TokenFlags) throws -> ()
/// FTS5_TOKEN_COLOCATED
public static let colocated = FTS5TokenFlags(rawValue: FTS5_TOKEN_COLOCATED)
}
/// A function that lets FTS5WrapperTokenizer notify tokens.
///
/// See FTS5WrapperTokenizer.accept(token:flags:tokenCallback:)
public typealias FTS5WrapperTokenCallback = (_ token: String, _ flags: FTS5TokenFlags) throws -> ()
/// The protocol for custom FTS5 tokenizers that wrap another tokenizer.
///
/// Types that adopt FTS5WrapperTokenizer don't have to implement the
/// low-level FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:).
///
/// Instead, they process regular Swift strings.
///
/// Here is the implementation for a trivial tokenizer that wraps the
/// built-in ascii tokenizer without any custom processing:
///
/// class TrivialAsciiTokenizer : FTS5WrapperTokenizer {
/// static let name = "trivial"
/// let wrappedTokenizer: FTS5Tokenizer
///
/// init(db: Database, arguments: [String]) throws {
/// wrappedTokenizer = try db.makeTokenizer(.ascii())
/// }
///
/// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws {
/// try tokenCallback(token, flags)
/// }
/// }
public protocol FTS5WrapperTokenizer : FTS5CustomTokenizer {
/// The wrapped tokenizer
var wrappedTokenizer: FTS5Tokenizer { get }
/// The protocol for custom FTS5 tokenizers that wrap another tokenizer.
/// Given a token produced by the wrapped tokenizer, notifies customized
/// tokens to the `tokenCallback` function.
///
/// Types that adopt FTS5WrapperTokenizer don't have to implement the
/// low-level FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:).
/// For example:
///
/// Instead, they process regular Swift strings.
///
/// Here is the implementation for a trivial tokenizer that wraps the
/// built-in ascii tokenizer without any custom processing:
///
/// class TrivialAsciiTokenizer : FTS5WrapperTokenizer {
/// static let name = "trivial"
/// let wrappedTokenizer: FTS5Tokenizer
///
/// init(db: Database, arguments: [String]) throws {
/// wrappedTokenizer = try db.makeTokenizer(.ascii())
/// }
///
/// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws {
/// try tokenCallback(token, flags)
/// }
/// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws {
/// // pass through:
/// try tokenCallback(token, flags)
/// }
public protocol FTS5WrapperTokenizer : FTS5CustomTokenizer {
/// The wrapped tokenizer
var wrappedTokenizer: FTS5Tokenizer { get }
/// Given a token produced by the wrapped tokenizer, notifies customized
/// tokens to the `tokenCallback` function.
///
/// For example:
///
/// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws {
/// // pass through:
/// try tokenCallback(token, flags)
/// }
///
/// When implementing the accept method, there are a two rules
/// to observe:
///
/// 1. Errors thrown by the tokenCallback function must not be caught.
///
/// 2. The input `flags` should be given unmodified to the tokenCallback
/// function, unless you union it with the .colocated flag when the
/// tokenizer produces synonyms (see
/// https://www.sqlite.org/fts5.html#synonym_support).
///
/// - parameters:
/// - token: A token produced by the wrapped tokenizer
/// - flags: Flags that tell SQLite how to register a token.
/// - tokenization: The reason why FTS5 is requesting tokenization.
/// - tokenCallback: The function to call for each customized token.
func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws
}
private struct FTS5WrapperContext {
let tokenizer: FTS5WrapperTokenizer
let context: UnsafeMutableRawPointer?
let tokenization: FTS5Tokenization
let tokenCallback: FTS5TokenCallback
}
extension FTS5WrapperTokenizer {
/// Default implementation
public func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 {
// `tokenCallback` is @convention(c). This requires a little setup
// in order to transfer context.
var customContext = FTS5WrapperContext(
tokenizer: self,
context: context,
tokenization: tokenization,
tokenCallback: tokenCallback)
return withUnsafeMutablePointer(to: &customContext) { customContextPointer in
// Invoke wrappedTokenizer
return wrappedTokenizer.tokenize(context: customContextPointer, tokenization: tokenization, pText: pText, nText: nText) { (customContextPointer, tokenFlags, pToken, nToken, iStart, iEnd) in
// Extract token produced by wrapped tokenizer
guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else {
return 0 // SQLITE_OK
}
// Extract context
let customContext = customContextPointer!.assumingMemoryBound(to: FTS5WrapperContext.self).pointee
let tokenizer = customContext.tokenizer
let context = customContext.context
let tokenization = customContext.tokenization
let tokenCallback = customContext.tokenCallback
// Process token produced by wrapped tokenizer
do {
try tokenizer.accept(
token: token,
flags: FTS5TokenFlags(rawValue: tokenFlags),
for: tokenization,
tokenCallback: { (token, flags) in
// Turn token into bytes
return try ContiguousArray(token.utf8).withUnsafeBufferPointer { buffer in
guard let addr = buffer.baseAddress else {
return
}
let pToken = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self)
let nToken = Int32(buffer.count)
// Inject token bytes into SQLite
let code = tokenCallback(context, flags.rawValue, pToken, nToken, iStart, iEnd)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: "token callback failed")
}
///
/// When implementing the accept method, there are a two rules
/// to observe:
///
/// 1. Errors thrown by the tokenCallback function must not be caught.
///
/// 2. The input `flags` should be given unmodified to the tokenCallback
/// function, unless you union it with the .colocated flag when the
/// tokenizer produces synonyms (see
/// https://www.sqlite.org/fts5.html#synonym_support).
///
/// - parameters:
/// - token: A token produced by the wrapped tokenizer
/// - flags: Flags that tell SQLite how to register a token.
/// - tokenization: The reason why FTS5 is requesting tokenization.
/// - tokenCallback: The function to call for each customized token.
func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws
}
private struct FTS5WrapperContext {
let tokenizer: FTS5WrapperTokenizer
let context: UnsafeMutableRawPointer?
let tokenization: FTS5Tokenization
let tokenCallback: FTS5TokenCallback
}
extension FTS5WrapperTokenizer {
/// Default implementation
public func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 {
// `tokenCallback` is @convention(c). This requires a little setup
// in order to transfer context.
var customContext = FTS5WrapperContext(
tokenizer: self,
context: context,
tokenization: tokenization,
tokenCallback: tokenCallback)
return withUnsafeMutablePointer(to: &customContext) { customContextPointer in
// Invoke wrappedTokenizer
return wrappedTokenizer.tokenize(context: customContextPointer, tokenization: tokenization, pText: pText, nText: nText) { (customContextPointer, tokenFlags, pToken, nToken, iStart, iEnd) in
// Extract token produced by wrapped tokenizer
guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else {
return 0 // SQLITE_OK
}
// Extract context
let customContext = customContextPointer!.assumingMemoryBound(to: FTS5WrapperContext.self).pointee
let tokenizer = customContext.tokenizer
let context = customContext.context
let tokenization = customContext.tokenization
let tokenCallback = customContext.tokenCallback
// Process token produced by wrapped tokenizer
do {
try tokenizer.accept(
token: token,
flags: FTS5TokenFlags(rawValue: tokenFlags),
for: tokenization,
tokenCallback: { (token, flags) in
// Turn token into bytes
return try ContiguousArray(token.utf8).withUnsafeBufferPointer { buffer in
guard let addr = buffer.baseAddress else {
return
}
})
return SQLITE_OK
} catch let error as DatabaseError {
return error.extendedResultCode.rawValue
} catch {
return SQLITE_ERROR
}
let pToken = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self)
let nToken = Int32(buffer.count)
// Inject token bytes into SQLite
let code = tokenCallback(context, flags.rawValue, pToken, nToken, iStart, iEnd)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: "token callback failed")
}
}
})
return SQLITE_OK
} catch let error as DatabaseError {
return error.extendedResultCode.rawValue
} catch {
return SQLITE_ERROR
}
}
}
}
}
#endif

143
GRDB/Fixit/GRDB-4.0.swift Normal file
View File

@ -0,0 +1,143 @@
import Dispatch
// Fixits for changes introduced by GRDB 4.0.0
extension Cursor {
@available(*, unavailable, renamed: "compactMap")
public func flatMap<ElementOfResult>(_ transform: @escaping (Element) throws -> ElementOfResult?) -> MapCursor<FilterCursor<MapCursor<Self, ElementOfResult?>>, ElementOfResult> { preconditionFailure() }
}
extension DatabaseWriter {
@available(*, unavailable, message: "Use concurrentRead instead")
public func readFromCurrentState(_ block: @escaping (Database) -> Void) throws { preconditionFailure() }
}
extension ValueObservation {
@available(*, unavailable, message: "Provide the reducer in a (Database) -> Reducer closure")
public static func tracking(_ regions: DatabaseRegionConvertible..., reducer: Reducer) -> ValueObservation { preconditionFailure() }
@available(*, unavailable, message: "Use distinctUntilChanged() instead")
public static func tracking<Value>(_ regions: DatabaseRegionConvertible..., fetchDistinct fetch: @escaping (Database) throws -> Value) -> ValueObservation<DistinctUntilChangedValueReducer<RawValueReducer<Value>>> where Value: Equatable { preconditionFailure() }
}
@available(*, unavailable, renamed: "FastDatabaseValueCursor")
public typealias ColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> = FastDatabaseValueCursor<Value>
@available(*, unavailable, renamed: "FastNullableDatabaseValueCursor")
public typealias NullableColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> = FastNullableDatabaseValueCursor<Value>
extension Database {
@available(*, unavailable, renamed: "execute(sql:arguments:)")
public func execute(_ sql: String, arguments: StatementArguments? = nil) throws { preconditionFailure() }
@available(*, unavailable, renamed: "makeSelectStatement(sql:)")
public func makeSelectStatement(_ sql: String) throws -> SelectStatement { preconditionFailure() }
@available(*, unavailable, renamed: "cachedSelectStatement(sql:)")
public func cachedSelectStatement(_ sql: String) throws -> SelectStatement { preconditionFailure() }
@available(*, unavailable, renamed: "makeUpdateStatement(sql:)")
public func makeUpdateStatement(_ sql: String) throws -> UpdateStatement { preconditionFailure() }
@available(*, unavailable, renamed: "cachedUpdateStatement(sql:)")
public func cachedUpdateStatement(_ sql: String) throws -> UpdateStatement { preconditionFailure() }
}
extension DatabaseValueConvertible {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> { preconditionFailure() }
@available(*, unavailable, renamed: "fetchAll(_:sql:arguments:adapter:)")
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { preconditionFailure() }
@available(*, unavailable, renamed: "fetchOne(_:sql:arguments:adapter:)")
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { preconditionFailure() }
}
extension Optional where Wrapped: DatabaseValueConvertible {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> { preconditionFailure() }
@available(*, unavailable, renamed: "fetchAll(_:sql:arguments:adapter:)")
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] { preconditionFailure() }
}
extension Row {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RowCursor { preconditionFailure() }
@available(*, unavailable, renamed: "fetchAll(_:sql:arguments:adapter:)")
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Row] { preconditionFailure() }
@available(*, unavailable, renamed: "fetchOne(_:sql:arguments:adapter:)")
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Row? { preconditionFailure() }
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor<Self> { preconditionFailure() }
}
extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConvertible {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor<Wrapped> { preconditionFailure() }
}
extension FetchableRecord {
@available(*, unavailable, renamed: "fetchCursor(_:sql:arguments:adapter:)")
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RecordCursor<Self> { preconditionFailure() }
@available(*, unavailable, renamed: "fetchAll(_:sql:arguments:adapter:)")
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { preconditionFailure() }
@available(*, unavailable, renamed: "fetchOne(_:sql:arguments:adapter:)")
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { preconditionFailure() }
}
extension SQLRequest {
@available(*, unavailable, renamed: "init(sql:arguments:adapter:cached:)")
public init(_ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil, cached: Bool = false) { preconditionFailure() }
}
extension SQLExpressionLiteral {
@available(*, unavailable, renamed: "init(sql:arguments:)")
public init(_ sql: String, arguments: StatementArguments? = nil) { preconditionFailure() }
}
extension SQLExpression {
@available(*, unavailable, message: "Use sqlLiteral property instead")
public var literal: SQLExpressionLiteral { preconditionFailure() }
}
extension FTS3TokenizerDescriptor {
@available(*, unavailable, renamed: "unicode61(diacritics:separators:tokenCharacters:)")
public static func unicode61(removeDiacritics: Bool, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor { preconditionFailure() }
}
#if SQLITE_ENABLE_FTS5
extension FTS5TokenizerDescriptor {
@available(*, unavailable, renamed: "unicode61(diacritics:separators:tokenCharacters:)")
public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS5TokenizerDescriptor { preconditionFailure() }
}
#endif
extension DatabaseValue {
@available(*, unavailable)
public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T: DatabaseValueConvertible { preconditionFailure() }
@available(*, unavailable)
public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T: DatabaseValueConvertible { preconditionFailure() }
}
extension ValueScheduling {
@available(*, unavailable, renamed: "async(onQueue:startImmediately:)")
public static func onQueue(_ queue: DispatchQueue, startImmediately: Bool) -> ValueScheduling { preconditionFailure() }
}
extension ValueObservation {
@available(*, unavailable, message: "Observation extent is controlled by the lifetime of observers returned by the start() method.")
public var extent: Database.TransactionObservationExtent {
get { preconditionFailure() }
set { preconditionFailure() }
}
}

View File

@ -1,19 +0,0 @@
import Foundation
@available(*, unavailable, renamed:"Database.ForeignKeyAction")
public typealias SQLForeignKeyAction = Database.ForeignKeyAction
@available(*, unavailable, renamed:"Database.ColumnType")
public typealias SQLColumnType = Database.ColumnType
@available(*, unavailable, renamed:"Database.ConflictResolution")
public typealias SQLConflictResolution = Database.ConflictResolution
@available(*, unavailable, renamed:"Database.CollationName")
public typealias SQLCollation = Database.CollationName
@available(*, unavailable, renamed:"SQLSpecificExpressible")
public typealias _SpecificSQLExpressible = SQLSpecificExpressible
@available(*, unavailable, renamed:"SQLExpression")
public typealias _SQLExpression = SQLExpression

View File

@ -1,55 +0,0 @@
extension Row {
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch<Request: FetchRequest>(_ db: Database, _ request: Request) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
}
extension DatabaseValueConvertible {
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch<Request: FetchRequest>(_ db: Database, _ request: Request) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
}
extension Optional where Wrapped: DatabaseValueConvertible {
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch<Request: FetchRequest>(_ db: Database, _ request: Request) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
}
extension QueryInterfaceRequest {
@available(*, unavailable, renamed:"fetchCursor")
public func fetch(_ db: Database) -> Any { preconditionFailure() }
}
extension FetchableRecord {
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch<Request: FetchRequest>(_ db: Database, _ request: Request) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() }
}
extension FetchableRecord where Self: TableRecord {
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database) -> Any { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) -> Any where Sequence.Iterator.Element: DatabaseValueConvertible { preconditionFailure() }
@available(*, unavailable, renamed:"fetchCursor")
public static func fetch(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) -> Any { preconditionFailure() }
}
@available(*, unavailable, message:"DatabaseSequence has been replaced by Cursor.")
public struct DatabaseSequence<T> { }
@available(*, unavailable, message:"DatabaseIterator has been replaced by Cursor.")
public struct DatabaseIterator<T> { }

View File

@ -1,4 +0,0 @@
extension DatabaseError {
@available(*, unavailable, renamed:"resultCode")
public var code: Int32 { return 0 }
}

View File

@ -1,9 +0,0 @@
extension Database {
@available(*, unavailable, renamed:"inTransaction")
public func writeInTransaction(_ kind: Database.TransactionKind? = nil, _ block: (Database) throws -> Database.TransactionCompletion) throws { }
}
extension DatabaseValue {
@available(*, unavailable, message:"DatabaseSequence has been replaced by Cursor.")
public func value() -> Any { preconditionFailure() }
}

View File

@ -1,4 +0,0 @@
extension FetchRequest {
@available(*, unavailable, message:"Use QueryInterfaceRequest.asRequest(of:), or AnyFetchRequest")
public func bound<T>(to type: T.Type) -> AnyFetchRequest<T> { preconditionFailure() }
}

View File

@ -1,28 +0,0 @@
extension Row {
@available(*, unavailable, message:"use subscript instead: row[index]")
public func value(atIndex index: Int) -> DatabaseValueConvertible? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[index]")
public func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[index]")
public func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value(named name: String) -> DatabaseValueConvertible? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value<Value: DatabaseValueConvertible>(named name: String) -> Value? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value<Value: DatabaseValueConvertible>(named name: String) -> Value { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value(_ column: Column) -> DatabaseValueConvertible? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value? { preconditionFailure() }
@available(*, unavailable, message:"use subscript instead: row[column]")
public func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value { preconditionFailure() }
}

View File

@ -1,85 +0,0 @@
/// :nodoc:
@available(*, unavailable, renamed:"FetchRequest")
public typealias Request = FetchRequest
/// :nodoc:
@available(*, unavailable, renamed:"FetchableRecord")
public typealias RowConvertible = FetchableRecord
/// :nodoc:
@available(*, unavailable, renamed:"TableRecord")
public typealias TableMapping = TableRecord
/// :nodoc:
@available(*, unavailable, renamed:"MutablePersistableRecord")
public typealias MutablePersistable = MutablePersistableRecord
/// :nodoc:
@available(*, unavailable, renamed:"PersistableRecord")
public typealias Persistable = PersistableRecord
/// :nodoc:
@available(*, unavailable, renamed:"TableAlias")
public typealias SQLTableQualifier = TableAlias
extension Database {
/// :nodoc:
@available(*, unavailable, message: "Use db.columns(in: tableName).count instead")
public func columnCount(in tableName: String) throws -> Int { preconditionFailure() }
}
extension SelectStatement {
/// :nodoc:
@available(*, unavailable, renamed:"DatabaseRegion")
public typealias SelectionInfo = DatabaseRegion
/// :nodoc:
@available(*, unavailable, renamed:"databaseRegion")
public var selectionInfo: DatabaseRegion { preconditionFailure() }
/// :nodoc:
@available(*, unavailable, renamed:"databaseRegion")
public var fetchedRegion: DatabaseRegion { preconditionFailure() }
}
extension DatabaseEventKind {
/// :nodoc:
@available(*, unavailable, message: "Use DatabaseRegion.isModified(byEventsOfKind:) instead")
public func impacts(_ region: DatabaseRegion) -> Bool { preconditionFailure() }
}
extension Record {
/// :nodoc:
@available(*, unavailable, renamed: "hasDatabaseChanges")
public var hasPersistentChangedValues: Bool { preconditionFailure() }
/// :nodoc:
@available(*, unavailable, renamed: "databaseChanges")
public var persistentChangedValues: [String: DatabaseValue?] { preconditionFailure() }
}
@available(*, unavailable, message: "Use changes methods defined on the MutablePersistableRecord protocol: databaseEquals(_:), databaseChanges(from:), updateChanges(from:)")
public final class RecordBox<T: FetchableRecord & MutablePersistableRecord>: Record { }
extension MutablePersistableRecord {
/// :nodoc:
@available(*, unavailable, renamed: "databaseEquals")
public func databaseEqual(_ record: Self) -> Bool { preconditionFailure() }
}
extension Row {
/// :nodoc:
@available(*, unavailable, message: "Use row.scopes.names instead")
var scopeNames: Set<String> { preconditionFailure() }
/// :nodoc:
@available(*, unavailable, message: "Use row.scopes[name] instead")
public func scoped(on name: String) -> Row? { preconditionFailure() }
}
extension FetchRequest {
/// :nodoc:
@available(*, unavailable, renamed:"databaseRegion(_:)")
public func fetchedRegion(_ db: Database) throws -> DatabaseRegion { preconditionFailure() }
}

View File

@ -1,210 +0,0 @@
//
// Fixits-Swift2.swift
// GRDB
//
// Created by Swiftlyfalling.
//
// Provides automatic renaming Fix-Its for many of the Swift 2.x -> Swift 3 GRDB API changes.
// Consult the CHANGELOG.md and documentation for details on all of the changes.
//
import Foundation
#if os(iOS)
import UIKit
#endif
// Database Connections
@available(*, unavailable, renamed:"Database.BusyMode")
public typealias BusyMode = Database.BusyMode
@available(*, unavailable, renamed:"Database.CheckpointMode")
public typealias CheckpointMode = Database.CheckpointMode
@available(*, unavailable, renamed:"Database.TransactionKind")
public typealias TransactionKind = Database.TransactionKind
@available(*, unavailable, renamed:"Database.TransactionCompletion")
public typealias TransactionCompletion = Database.TransactionCompletion
@available(*, unavailable, renamed:"Database.BusyCallback")
public typealias BusyCallback = Database.BusyCallback
extension DatabasePool {
#if os(iOS)
@available(*, unavailable, renamed:"setupMemoryManagement(in:)")
public func setupMemoryManagement(application: UIApplication) { }
#endif
#if SQLITE_HAS_CODEC
@available(*, unavailable, renamed:"change(passphrase:)")
public func changePassphrase(_ passphrase: String) throws { }
#endif
}
extension DatabaseQueue {
#if os(iOS)
@available(*, unavailable, renamed:"setupMemoryManagement(in:)")
public func setupMemoryManagement(application: UIApplication) { }
#endif
#if SQLITE_HAS_CODEC
@available(*, unavailable, renamed:"change(passphrase:)")
public func changePassphrase(_ passphrase: String) throws { }
#endif
}
// SQL Functions
extension Database {
@available(*, unavailable, renamed:"add(function:)")
public func addFunction(_ function: DatabaseFunction) { }
@available(*, unavailable, renamed:"remove(function:)")
public func removeFunction(_ function: DatabaseFunction) { }
}
extension DatabasePool {
@available(*, unavailable, renamed:"add(function:)")
public func addFunction(_ function: DatabaseFunction) { }
@available(*, unavailable, renamed:"remove(function:)")
public func removeFunction(_ function: DatabaseFunction) { }
}
extension DatabaseQueue {
@available(*, unavailable, renamed:"add(function:)")
public func addFunction(_ function: DatabaseFunction) { }
@available(*, unavailable, renamed:"remove(function:)")
public func removeFunction(_ function: DatabaseFunction) { }
}
extension DatabaseReader {
@available(*, unavailable, renamed:"add(function:)")
public func addFunction(_ function: DatabaseFunction) { }
@available(*, unavailable, renamed:"remove(function:)")
public func removeFunction(_ function: DatabaseFunction) { }
}
extension DatabaseFunction {
@available(*, unavailable, renamed:"capitalize")
public static let capitalizedString = capitalize
@available(*, unavailable, renamed:"lowercase")
public static let lowercaseString = lowercase
@available(*, unavailable, renamed:"uppercase")
public static let uppercaseString = uppercase
}
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
extension DatabaseFunction {
@available(*, unavailable, renamed:"localizedCapitalize")
public static let localizedCapitalizedString = localizedCapitalize
@available(*, unavailable, renamed:"localizedLowercase")
public static let localizedLowercaseString = localizedLowercase
@available(*, unavailable, renamed:"localizedUppercase")
public static let localizedUppercaseString = localizedUppercase
}
// SQL Collations
extension Database {
@available(*, unavailable, renamed:"add(collation:)")
public func addCollation(_ collation: DatabaseCollation) { }
@available(*, unavailable, renamed:"remove(collation:)")
public func removeCollation(_ collation: DatabaseCollation) { }
}
extension DatabasePool {
@available(*, unavailable, renamed:"add(collation:)")
public func addCollation(_ collation: DatabaseCollation) { }
@available(*, unavailable, renamed:"remove(collation:)")
public func removeCollation(_ collation: DatabaseCollation) { }
}
extension DatabaseQueue {
@available(*, unavailable, renamed:"add(collation:)")
public func addCollation(_ collation: DatabaseCollation) { }
@available(*, unavailable, renamed:"remove(collation:)")
public func removeCollation(_ collation: DatabaseCollation) { }
}
extension DatabaseReader {
@available(*, unavailable, renamed:"add(collation:)")
public func addCollation(_ collation: DatabaseCollation) { }
@available(*, unavailable, renamed:"remove(collation:)")
public func removeCollation(_ collation: DatabaseCollation) { }
}
// Prepared Statements
extension Database {
@available(*, unavailable, renamed:"makeSelectStatement(_:)")
func selectStatement(_ sql: String) throws -> SelectStatement { preconditionFailure() }
@available(*, unavailable, renamed:"makeUpdateStatement(_:)")
func updateStatement(_ sql: String) throws -> UpdateStatement { preconditionFailure() }
}
extension Statement {
@available(*, unavailable, renamed:"validate(arguments:)")
public func validateArguments(_ arguments: StatementArguments) throws { }
}
// Transaction Observers
extension Database {
@available(*, unavailable, message:"Use add(transactionObserver:) instead. Database events filtering is now performed by transaction observers themselves.")
public func addTransactionObserver(_ transactionObserver: TransactionObserver, forDatabaseEvents filter: ((DatabaseEventKind) -> Bool)? = nil) { }
@available(*, unavailable, renamed:"remove(transactionObserver:)")
public func removeTransactionObserver(_ transactionObserver: TransactionObserver) { }
}
extension DatabaseWriter {
@available(*, unavailable, message:"Use add(transactionObserver:) instead. Database events filtering is now performed by transaction observers themselves.")
public func addTransactionObserver(_ transactionObserver: TransactionObserver, forDatabaseEvents filter: ((DatabaseEventKind) -> Bool)? = nil) { }
@available(*, unavailable, renamed:"remove(transactionObserver:)")
public func removeTransactionObserver(_ transactionObserver: TransactionObserver) { }
}
@available(*, unavailable, renamed:"TransactionObserver")
public typealias TransactionObserverType = TransactionObserver
// Query Interface
@available(*, unavailable, renamed:"Column")
public typealias SQLColumn = Column
/// :nodoc:
extension SQLSpecificExpressible {
@available(*, unavailable, renamed:"capitalized")
public var capitalizedString: SQLExpression { get { return capitalized } }
@available(*, unavailable, renamed:"lowercased")
public var lowercaseString: SQLExpression { get { return lowercased } }
@available(*, unavailable, renamed:"uppercased")
public var uppercaseString: SQLExpression { get { return uppercased } }
}
/// :nodoc:
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
extension SQLSpecificExpressible {
@available(*, unavailable, renamed:"localizedCapitalized")
public var localizedCapitalizedString: SQLExpression { get { return localizedCapitalized } }
@available(*, unavailable, renamed:"localizedLowercased")
public var localizedLowercaseString: SQLExpression { get { return localizedLowercased } }
@available(*, unavailable, renamed:"localizedUppercased")
public var localizedUppercaseString: SQLExpression { get { return localizedUppercased } }
}

View File

@ -100,7 +100,7 @@ public struct DatabaseMigrator {
/// t.autoIncrementedPrimaryKey("id")
/// t.column("name", .text).notNull()
/// }
/// try db.execute("INSERT INTO new_player SELECT * FROM player")
/// try db.execute(sql: "INSERT INTO new_player SELECT * FROM player")
/// try db.drop(table: "player")
/// try db.rename(table: "new_player", to: "player")
/// }
@ -119,7 +119,7 @@ public struct DatabaseMigrator {
registerMigration(Migration(identifier: identifier, disabledForeignKeyChecks: true, migrate: migrate))
}
#else
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
/// Registers an advanced migration, as described at https://www.sqlite.org/lang_altertable.html#otheralter
///
/// // Add a NOT NULL constraint on players.name:
@ -128,7 +128,7 @@ public struct DatabaseMigrator {
/// t.autoIncrementedPrimaryKey("id")
/// t.column("name", .text).notNull()
/// }
/// try db.execute("INSERT INTO new_player SELECT * FROM player")
/// try db.execute(sql: "INSERT INTO new_player SELECT * FROM player")
/// try db.drop(table: "player")
/// try db.rename(table: "new_player", to: "player")
/// }
@ -237,11 +237,11 @@ public struct DatabaseMigrator {
}
private func setupMigrations(_ db: Database) throws {
try db.execute("CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY)")
}
private func appliedIdentifiers(_ db: Database) throws -> Set<String> {
return try Set(String.fetchAll(db, "SELECT identifier FROM grdb_migrations"))
return try Set(String.fetchAll(db, sql: "SELECT identifier FROM grdb_migrations"))
}
private func runMigrations(_ db: Database, upTo targetIdentifier: String) throws {

View File

@ -17,7 +17,7 @@ struct Migration {
self.migrate = migrate
}
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
// PRAGMA foreign_key_check was introduced in SQLite 3.7.16 http://www.sqlite.org/changes.html#version_3_7_16
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
init(identifier: String, disabledForeignKeyChecks: Bool, migrate: @escaping (Database) throws -> Void) {
@ -28,7 +28,7 @@ struct Migration {
#endif
func run(_ db: Database) throws {
if try disabledForeignKeyChecks && (Bool.fetchOne(db, "PRAGMA foreign_keys") ?? false) {
if try disabledForeignKeyChecks && (Bool.fetchOne(db, sql: "PRAGMA foreign_keys") ?? false) {
try runWithDisabledForeignKeys(db)
} else {
try runWithoutDisabledForeignKeys(db)
@ -50,7 +50,7 @@ struct Migration {
//
// > 1. If foreign key constraints are enabled, disable them using
// > PRAGMA foreign_keys=OFF.
try db.execute("PRAGMA foreign_keys = OFF")
try db.execute(sql: "PRAGMA foreign_keys = OFF")
// > 2. Start a transaction.
try db.inTransaction(.immediate) {
@ -60,7 +60,7 @@ struct Migration {
// > 10. If foreign key constraints were originally enabled then run PRAGMA
// > foreign_key_check to verify that the schema change did not break any foreign key
// > constraints.
if try Row.fetchOne(db, "PRAGMA foreign_key_check") != nil {
if try Row.fetchOne(db, sql: "PRAGMA foreign_key_check") != nil {
// https://www.sqlite.org/pragma.html#pragma_foreign_key_check
//
// PRAGMA foreign_key_check does not return an error,
@ -76,10 +76,10 @@ struct Migration {
}
// > 12. If foreign keys constraints were originally enabled, reenable them now.
try db.execute("PRAGMA foreign_keys = ON")
try db.execute(sql: "PRAGMA foreign_keys = ON")
}
private func insertAppliedIdentifier(_ db: Database) throws {
try db.execute("INSERT INTO grdb_migrations (identifier) VALUES (?)", arguments: [identifier])
try db.execute(sql: "INSERT INTO grdb_migrations (identifier) VALUES (?)", arguments: [identifier])
}
}

View File

@ -28,28 +28,16 @@ public protocol Association: DerivableRequest {
/// }
associatedtype RowDecoder
// Association is a protocol, not a struct.
// This is because we want associations to be richly typed.
// Yet, we don't want to pollute user code with implementation details of
// associations. So let's hide all this stuff behind the _impl property:
/// :nodoc:
var sqlAssociation: SQLAssociation { get }
/// :nodoc:
associatedtype Impl: AssociationImpl
/// :nodoc:
var _impl: Impl { get }
/// :nodoc:
init(_impl: Impl)
init(sqlAssociation: SQLAssociation)
}
extension Association {
private func mapImpl(_ transform: (Impl) throws -> Impl) rethrows -> Self {
return try Self.init(_impl: transform(_impl))
}
private func mapRelation(_ transform: (SQLRelation) -> SQLRelation) -> Self {
return mapImpl { $0.mapRelation(transform) }
return Self.init(sqlAssociation: sqlAssociation.mapRelation(transform))
}
}
@ -80,7 +68,7 @@ extension Association {
/// let team: Team = row["custom"]
/// }
var key: String {
return _impl.key
return sqlAssociation.key
}
/// Creates an association which selects *selection*.
@ -191,7 +179,7 @@ extension Association {
/// let team: Team = row["custom"]
/// }
public func forKey(_ key: String) -> Self {
return mapImpl { $0.forKey(key) }
return Self.init(sqlAssociation: sqlAssociation.forKey(key))
}
/// Creates an association with the given key.
@ -258,151 +246,270 @@ extension Association {
/// associated record are selected. The returned association does not
/// require that the associated database table contains a matching row.
public func including<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
return mapRelation { association._impl.joinedRelation($0, joinOperator: .optional) }
return mapRelation { association.sqlAssociation.relation(from: $0, required: false) }
}
/// Creates an association that includes another one. The columns of the
/// associated record are selected. The returned association requires
/// that the associated database table contains a matching row.
public func including<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
return mapRelation { association._impl.joinedRelation($0, joinOperator: .required) }
return mapRelation { association.sqlAssociation.relation(from: $0, required: true) }
}
/// Creates an association that joins another one. The columns of the
/// associated record are not selected. The returned association does not
/// require that the associated database table contains a matching row.
public func joining<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
return mapRelation { association.select([])._impl.joinedRelation($0, joinOperator: .optional) }
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, required: false) }
}
/// Creates an association that joins another one. The columns of the
/// associated record are not selected. The returned association requires
/// that the associated database table contains a matching row.
public func joining<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
return mapRelation { association.select([])._impl.joinedRelation($0, joinOperator: .required) }
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, required: true) }
}
}
extension Association where OriginRowDecoder: MutablePersistableRecord {
/// Support for MutablePersistableRecord.request(for:).
///
/// For example:
///
/// struct Team: {
/// static let players = hasMany(Player.self)
/// var players: QueryInterfaceRequest<Player> {
/// return request(for: Team.players)
/// }
/// }
///
/// let team: Team = ...
/// let players = try team.players.fetchAll(db) // [Player]
func request(from record: OriginRowDecoder) -> QueryInterfaceRequest<RowDecoder> {
// Goal: turn `JOIN association ON association.recordId = record.id`
// into a regular request `SELECT * FROM association WHERE association.recordId = 123`
// We need table aliases to build the joining condition
let associationAlias = TableAlias()
let recordAlias = TableAlias()
// Turn the association query into a query interface request:
// JOIN association -> SELECT FROM association
return QueryInterfaceRequest(query: SQLSelectQuery(relation: _impl.relation))
// Turn the JOIN condition into a regular WHERE condition
.filter { db in
// Build a join condition: `association.recordId = record.id`
// We still need to replace `record.id` with the actual record id.
let joinExpression = try self._impl.joinCondition.sqlExpression(db, leftAlias: recordAlias, rightAlias: associationAlias)
// Serialize record: ["id": 123, ...]
// We do it as late as possible, when request is about to be
// executed, in order to support long-lived reference types.
let container = PersistenceContainer(record)
// Replace `record.id` with 123
return joinExpression.resolvedExpression(inContext: [recordAlias: container])
}
// We just added a condition qualified with associationAlias. Don't
// risk introducing conflicting aliases that would prevent the user
// from setting a custom alias name: force the same alias for the
// whole request.
.aliased(associationAlias)
}
// Allow association.filter(key: ...)
extension Association where Self: TableRequest, RowDecoder: TableRecord {
/// :nodoc:
public var databaseTableName: String { return RowDecoder.databaseTableName }
}
// MARK: - AssociationImpl
// MARK: - AssociationToMany
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// The protocol for implementation details of associations.
///
/// :nodoc:
public /* TODO: internal */ protocol AssociationImpl {
/// The association key
var key: String { get }
/// Creates an association with the given key.
func forKey(_ key: String) -> Self
/// Returns an association whose relation is transformed by the
/// given closure.
///
/// This method provides fundamental support for association derivation:
///
/// // Invokes Book.author.mapRelation { $0.filter(...) }
/// Book.author.filter(...)
func mapRelation(_ transform: (SQLRelation) -> SQLRelation) -> Self
/// Returns a relation joined with self.
///
/// This method provides fundamental support for joining methods.
func joinedRelation(_ relation: SQLRelation, joinOperator: JoinOperator) -> SQLRelation
// TODO: remove relation & joinCondition properties.
//
// They assume that an association is implemented as a direct join to an
// associated table. This is limiting: has-one-through and has-many-through
// associations can't be implemented in such context.
//
// Their impact is limited yet. Those propertise are currently only used by
// Association.request(from:). When this method gets a new implementation
// that does not need a direct join to an associated table, we'll be able to
// remove those properties.
var relation: SQLRelation { get }
var joinCondition: JoinCondition { get }
}
/// The base protocol for all associations that define a one-to-many connection.
public protocol AssociationToMany: Association { }
// MARK: -
/// The AssociationImpl shared by BelongsTo, HasOne, and HasMany, which is
/// implemented as a simple join.
///
/// :nodoc:
public /* TODO: internal */ struct JoinAssociationImpl: AssociationImpl {
public var key: String
public /* TODO: internal */ let joinCondition: JoinCondition
public /* TODO: internal */ var relation: SQLRelation
public func forKey(_ key: String) -> JoinAssociationImpl {
var assoc = self
assoc.key = key
return assoc
extension AssociationToMany where OriginRowDecoder: TableRecord {
private func makeAggregate(_ expression: SQLExpression) -> AssociationAggregate<OriginRowDecoder> {
return AssociationAggregate { request in
let tableAlias = TableAlias()
let request = request
.joining(optional: self.aliased(tableAlias))
.groupByPrimaryKey()
let expression = tableAlias[expression]
return (request: request, expression: expression)
}
}
public func mapRelation(_ transform: (SQLRelation) -> SQLRelation) -> JoinAssociationImpl {
var assoc = self
assoc.relation = transform(relation)
return assoc
/// The number of associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.count())
public var count: AssociationAggregate<OriginRowDecoder> {
return makeAggregate(SQLExpressionCountDistinct(Column.rowID)).aliased("\(key)Count")
}
public func joinedRelation(_ relation: SQLRelation, joinOperator: JoinOperator) -> SQLRelation {
let join = SQLJoin(
joinOperator: joinOperator,
joinCondition: joinCondition,
relation: self.relation)
return relation.appendingJoin(join, forKey: key)
/// An aggregate that is true if there exists no associated records.
///
/// For example:
///
/// Team.having(Team.players.isEmpty())
/// Team.having(!Team.players.isEmpty())
/// Team.having(Team.players.isEmpty() == false)
public var isEmpty: AssociationAggregate<OriginRowDecoder> {
return makeAggregate(SQLExpressionIsEmpty(SQLExpressionCountDistinct(Column.rowID)))
}
/// The average value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.average(Column("score")))
public func average(_ expression: SQLExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(SQLExpressionFunction(.avg, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("average\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The maximum value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.max(Column("score")))
public func max(_ expression: SQLExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(SQLExpressionFunction(.max, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("max\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The minimum value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.min(Column("score")))
public func min(_ expression: SQLExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(SQLExpressionFunction(.min, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("min\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The sum of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.min(Column("score")))
public func sum(_ expression: SQLExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(SQLExpressionFunction(.sum, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("\(key)\(column.name.uppercasingFirstCharacter)Sum")
} else {
return aggregate
}
}
}
// MARK: - AssociationToOne
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// The base protocol for all associations that define a one-to-one connection.
public protocol AssociationToOne: Association { }
// MARK: - SQLAssociation
/// An SQL association is a non-empty chain of joins from an "origin" table to
/// the "head" of the association. All tables between "origin" and "head" are
/// the "tail". The table that is immediately joined to "origin" is the "pivot":
///
/// // SELECT origin.*, head.*
/// // FROM origin
/// // JOIN pivot ON ... JOIN ... -- tail
/// // JOIN head ON ... -- head
/// origin.including(required: association)
///
/// When tail is empty, "pivot" and "head" are the same:
///
/// // SELECT origin.*, head.* FROM origin JOIN head ON ...
/// origin.including(required: association)
///
/// :nodoc:
public /* TODO: internal */ struct SQLAssociation {
// SQLAssociation is a non-empty array of association elements
private struct Element {
var key: String
var condition: SQLJoin.Condition
var relation: SQLRelation
}
private var head: Element
private var tail: [Element]
private var pivot: Element { return tail.last ?? head }
var key: String { return head.key }
private init(head: Element, tail: [Element]) {
self.head = head
self.tail = tail
}
init(key: String, condition: SQLJoin.Condition, relation: SQLRelation) {
head = Element(key: key, condition: condition, relation: relation)
tail = []
}
func forKey(_ key: String) -> SQLAssociation {
var result = self
result.head.key = key
return result
}
func mapRelation(_ transform: (SQLRelation) -> SQLRelation) -> SQLAssociation {
var result = self
result.head.relation = transform(head.relation)
return result
}
func appending(_ other: SQLAssociation) -> SQLAssociation {
var result = self
result.tail.append(other.head)
result.tail.append(contentsOf: other.tail)
return result
}
/// Support for joining methods joining(optional:), etc.
func relation(from origin: SQLRelation, required: Bool) -> SQLRelation {
let headJoin = SQLJoin(
isRequired: required,
condition: head.condition,
relation: head.relation)
// Recursion step: remove one element from tail by shifting the next
// element to the head.
//
// From:
// ... JOIN next JOIN head
// <-tail------> <-head-->
//
// We reduce into:
// ... JOIN next JOIN head
// <-tail-> <-head------------>
//
// Until the tail is empty:
guard let next = tail.first else {
return origin.appendingJoin(headJoin, forKey: head.key)
}
let nextRelation = next.relation.select([]).appendingJoin(headJoin, forKey: head.key)
let reducedHead = Element(key: next.key, condition: next.condition, relation: nextRelation)
let reducedTail = Array(tail.dropFirst())
let reducedAssociation = SQLAssociation(head: reducedHead, tail: reducedTail)
return reducedAssociation.relation(from: origin, required: required)
}
/// Support for (TableRecord & EncodableRecord).request(for:).
///
/// Returns a "reversed" relation:
///
/// // SELECT head.* FROM head JOIN ... JOIN pivot ON pivot.originId = 123
/// origin.request(for: association)
///
/// When tail is empty, "pivot" and "head" are the same:
///
/// // SELECT head.* FROM head WHERE head.originId = 123
/// origin.request(for: association)
func relation(to originTable: String, container originContainer: @escaping (Database) throws -> PersistenceContainer) -> SQLRelation {
// Build a "pivot" relation whose filter is the pivot condition
// injected with values contained in originContainer.
let pivotCondition = pivot.condition
let pivotAlias = TableAlias()
let pivotRelation = pivot.relation
.qualified(with: pivotAlias)
.filter { db in
let originAlias = TableAlias(tableName: originTable)
// Build a join condition: `association.originId = origin.id`
let joinExpression = try pivotCondition.sqlExpression(db, leftAlias: originAlias, rightAlias: pivotAlias)
// Replace `origin.id` with 123
return try joinExpression.resolvedExpression(inContext: [originAlias: originContainer(db)])
}
// We use elements backward: join conditions have to be reversed.
let reversedElements = zip([head] + tail, tail)
.map { Element(key: $1.key, condition: $0.condition.reversed, relation: $1.relation.select([])) }
.reversed()
// Empty tail?
guard var reversedHead = reversedElements.first else {
return pivotRelation
}
reversedHead.relation = pivotRelation.select([])
let reversedTail = Array(reversedElements.dropFirst())
let reversedAssociation = SQLAssociation(head: reversedHead, tail: reversedTail)
return reversedAssociation.relation(from: head.relation, required: true)
}
}

View File

@ -108,7 +108,7 @@ extension AssociationAggregate {
///
/// For example:
///
/// let request = Author.having(!Author.books.isEmpty)
/// Author.having(!Author.books.isEmpty)
public prefix func ! <RowDecoder>(aggregate: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = aggregate.prepare(request)
@ -120,7 +120,7 @@ public prefix func ! <RowDecoder>(aggregate: AssociationAggregate<RowDecoder>) -
///
/// For example:
///
/// let request = Author.having(Author.books.isEmpty && Author.paintings.isEmpty)
/// Author.having(Author.books.isEmpty && Author.paintings.isEmpty)
public func && <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -152,7 +152,7 @@ public func && <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.having(!Author.books.isEmpty || !Author.paintings.isEmpty)
/// Author.having(!Author.books.isEmpty || !Author.paintings.isEmpty)
public func || <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -185,7 +185,7 @@ public func || <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.having(Author.books.count == Author.paintings.count)
/// Author.having(Author.books.count == Author.paintings.count)
public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -198,7 +198,7 @@ public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associat
///
/// For example:
///
/// let request = Author.having(Author.books.count == 3)
/// Author.having(Author.books.count == 3)
public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -210,7 +210,7 @@ public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpre
///
/// For example:
///
/// let request = Author.having(3 == Author.books.count)
/// Author.having(3 == Author.books.count)
public func == <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -222,7 +222,7 @@ public func == <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.having(Author.books.isEmpty == false)
/// Author.having(Author.books.isEmpty == false)
public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Bool) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -234,7 +234,7 @@ public func == <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Bool) ->
///
/// For example:
///
/// let request = Author.having(false == Author.books.isEmpty)
/// Author.having(false == Author.books.isEmpty)
public func == <RowDecoder>(lhs: Bool, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -246,7 +246,7 @@ public func == <RowDecoder>(lhs: Bool, rhs: AssociationAggregate<RowDecoder>) ->
///
/// For example:
///
/// let request = Author.having(Author.books.count != Author.paintings.count)
/// Author.having(Author.books.count != Author.paintings.count)
public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -259,7 +259,7 @@ public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associat
///
/// For example:
///
/// let request = Author.having(Author.books.count != 3)
/// Author.having(Author.books.count != 3)
public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -271,7 +271,7 @@ public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpre
///
/// For example:
///
/// let request = Author.having(3 != Author.books.count)
/// Author.having(3 != Author.books.count)
public func != <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -283,7 +283,7 @@ public func != <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.having(Author.books.isEmpty != true)
/// Author.having(Author.books.isEmpty != true)
public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Bool) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -295,7 +295,7 @@ public func != <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Bool) ->
///
/// For example:
///
/// let request = Author.having(true != Author.books.isEmpty)
/// Author.having(true != Author.books.isEmpty)
public func != <RowDecoder>(lhs: Bool, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -307,7 +307,7 @@ public func != <RowDecoder>(lhs: Bool, rhs: AssociationAggregate<RowDecoder>) ->
///
/// For example:
///
/// let request = Author.having(Author.books.count === Author.paintings.count)
/// Author.having(Author.books.count === Author.paintings.count)
public func === <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -320,7 +320,7 @@ public func === <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associa
///
/// For example:
///
/// let request = Author.having(Author.books.count === 3)
/// Author.having(Author.books.count === 3)
public func === <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -332,7 +332,7 @@ public func === <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpr
///
/// For example:
///
/// let request = Author.having(3 === Author.books.count)
/// Author.having(3 === Author.books.count)
public func === <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -344,7 +344,7 @@ public func === <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowD
///
/// For example:
///
/// let request = Author.having(Author.books.count !== Author.paintings.count)
/// Author.having(Author.books.count !== Author.paintings.count)
public func !== <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -357,7 +357,7 @@ public func !== <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associa
///
/// For example:
///
/// let request = Author.having(Author.books.count !== 3)
/// Author.having(Author.books.count !== 3)
public func !== <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -369,7 +369,7 @@ public func !== <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpr
///
/// For example:
///
/// let request = Author.having(3 !== Author.books.count)
/// Author.having(3 !== Author.books.count)
public func !== <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -383,7 +383,7 @@ public func !== <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowD
///
/// For example:
///
/// let request = Author.having(Author.books.count <= Author.paintings.count)
/// Author.having(Author.books.count <= Author.paintings.count)
public func <= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -396,7 +396,7 @@ public func <= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associat
///
/// For example:
///
/// let request = Author.having(Author.books.count <= 3)
/// Author.having(Author.books.count <= 3)
public func <= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -408,7 +408,7 @@ public func <= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpre
///
/// For example:
///
/// let request = Author.having(3 <= Author.books.count)
/// Author.having(3 <= Author.books.count)
public func <= <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -420,7 +420,7 @@ public func <= <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.having(Author.books.count < Author.paintings.count)
/// Author.having(Author.books.count < Author.paintings.count)
public func < <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -433,7 +433,7 @@ public func < <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.having(Author.books.count < 3)
/// Author.having(Author.books.count < 3)
public func < <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -445,7 +445,7 @@ public func < <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.having(3 < Author.books.count)
/// Author.having(3 < Author.books.count)
public func < <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -457,7 +457,7 @@ public func < <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
///
/// For example:
///
/// let request = Author.having(Author.books.count > Author.paintings.count)
/// Author.having(Author.books.count > Author.paintings.count)
public func > <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -470,7 +470,7 @@ public func > <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.having(Author.books.count > 3)
/// Author.having(Author.books.count > 3)
public func > <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -482,7 +482,7 @@ public func > <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.having(3 > Author.books.count)
/// Author.having(3 > Author.books.count)
public func > <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -494,7 +494,7 @@ public func > <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
///
/// For example:
///
/// let request = Author.having(Author.books.count >= Author.paintings.count)
/// Author.having(Author.books.count >= Author.paintings.count)
public func >= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -507,7 +507,7 @@ public func >= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associat
///
/// For example:
///
/// let request = Author.having(Author.books.count >= 3)
/// Author.having(Author.books.count >= 3)
public func >= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -519,7 +519,7 @@ public func >= <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpre
///
/// For example:
///
/// let request = Author.having(3 >= Author.books.count)
/// Author.having(3 >= Author.books.count)
public func >= <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -533,7 +533,7 @@ public func >= <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDe
///
/// For example:
///
/// let request = Author.annotated(with: -Author.books.count)
/// Author.annotated(with: -Author.books.count)
public prefix func - <RowDecoder>(aggregate: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = aggregate.prepare(request)
@ -545,7 +545,7 @@ public prefix func - <RowDecoder>(aggregate: AssociationAggregate<RowDecoder>) -
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count + Author.paintings.count)
/// Author.annotated(with: Author.books.count + Author.paintings.count)
public func + <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -558,7 +558,7 @@ public func + <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count + 1)
/// Author.annotated(with: Author.books.count + 1)
public func + <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -570,7 +570,7 @@ public func + <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.annotated(with: 1 + Author.books.count)
/// Author.annotated(with: 1 + Author.books.count)
public func + <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -582,7 +582,7 @@ public func + <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count - Author.paintings.count)
/// Author.annotated(with: Author.books.count - Author.paintings.count)
public func - <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -595,7 +595,7 @@ public func - <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count - 1)
/// Author.annotated(with: Author.books.count - 1)
public func - <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -607,7 +607,7 @@ public func - <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.annotated(with: 1 - Author.books.count)
/// Author.annotated(with: 1 - Author.books.count)
public func - <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -619,7 +619,7 @@ public func - <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count * Author.paintings.count)
/// Author.annotated(with: Author.books.count * Author.paintings.count)
public func * <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -632,7 +632,7 @@ public func * <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count * 2)
/// Author.annotated(with: Author.books.count * 2)
public func * <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -644,7 +644,7 @@ public func * <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.annotated(with: 2 * Author.books.count)
/// Author.annotated(with: 2 * Author.books.count)
public func * <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -656,7 +656,7 @@ public func * <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count / Author.paintings.count)
/// Author.annotated(with: Author.books.count / Author.paintings.count)
public func / <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (lRequest, lExpression) = lhs.prepare(request)
@ -669,7 +669,7 @@ public func / <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: Associati
///
/// For example:
///
/// let request = Author.annotated(with: Author.books.count / 2)
/// Author.annotated(with: Author.books.count / 2)
public func / <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = lhs.prepare(request)
@ -681,7 +681,7 @@ public func / <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpres
///
/// For example:
///
/// let request = Author.annotated(with: 2 / Author.books.count)
/// Author.annotated(with: 2 / Author.books.count)
public func / <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDecoder>) -> AssociationAggregate<RowDecoder> {
return AssociationAggregate { request in
let (request, expression) = rhs.prepare(request)
@ -689,7 +689,21 @@ public func / <RowDecoder>(lhs: SQLExpressible, rhs: AssociationAggregate<RowDec
}
}
// MARK: - IFNULL(...)
/// Returns an aggregate that evaluates the `IFNULL` SQL function.
///
/// Team.annotated(with: Team.players.min(Column("score")) ?? 0)
public func ?? <RowDecoder>(lhs: AssociationAggregate<RowDecoder>, rhs: SQLExpressible) -> AssociationAggregate<RowDecoder> {
var aggregate = AssociationAggregate<RowDecoder> { request in
let (request, expression) = lhs.prepare(request)
return (request: request, expression: expression ?? rhs)
}
// Preserve alias
aggregate.alias = lhs.alias
return aggregate
}
// TODO: add support for ABS(aggregate)
// TODO: add support for LENGTH(aggregate)
// TODO: add support for IFNULL(aggregate, ...)
// TODO: add support for IFNULL(..., aggregate)

View File

@ -58,7 +58,7 @@
/// }
///
/// See ForeignKey for more information.
public struct BelongsToAssociation<Origin, Destination>: Association {
public struct BelongsToAssociation<Origin, Destination>: AssociationToOne {
/// :nodoc:
public typealias OriginRowDecoder = Origin
@ -66,19 +66,16 @@ public struct BelongsToAssociation<Origin, Destination>: Association {
public typealias RowDecoder = Destination
/// :nodoc:
public var _impl: JoinAssociationImpl
public var sqlAssociation: SQLAssociation
/// :nodoc:
public init(_impl: JoinAssociationImpl) {
self._impl = _impl
public init(sqlAssociation: SQLAssociation) {
self.sqlAssociation = sqlAssociation
}
}
// Allow BelongsToAssociation(...).filter(key: ...)
extension BelongsToAssociation: TableRequest where Destination: TableRecord {
/// :nodoc:
public var databaseTableName: String { return Destination.databaseTableName }
}
extension BelongsToAssociation: TableRequest where Destination: TableRecord { }
extension TableRecord {
/// Creates a "Belongs To" association between Self and the
@ -110,10 +107,10 @@ extension TableRecord {
/// print("\(bookInfo.book.title) by \(bookInfo.author.name)")
/// }
///
/// It is recommended that you define, alongside the association, a property
/// with the same name:
/// It is recommended that you define, alongside the static association, a
/// property with the same name:
///
/// struct Book: TableRecord {
/// struct Book: TableRecord, EncodableRecord {
/// static let author = belongsTo(Author.self)
/// var author: QueryInterfaceRequest<Author> {
/// return request(for: Book.author)
@ -149,13 +146,13 @@ extension TableRecord {
destinationTable: Destination.databaseTableName,
foreignKey: foreignKey)
let joinCondition = JoinCondition(
let condition = SQLJoin.Condition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: true)
return BelongsToAssociation(_impl: JoinAssociationImpl(
return BelongsToAssociation(sqlAssociation: SQLAssociation(
key: key ?? Destination.databaseTableName,
joinCondition: joinCondition,
condition: condition,
relation: Destination.all().relation))
}
}

View File

@ -19,14 +19,15 @@ struct ForeignKeyRequest: Equatable {
self.destinationColumns = foreignKey?.destinationColumns
}
func fetch(_ db: Database) throws -> ForeignKeyInfo {
/// The (origin, destination) column pairs that join a left table to a right table.
func fetchMapping(_ db: Database) throws -> [(origin: String, destination: String)] {
if let originColumns = originColumns, let destinationColumns = destinationColumns {
// Total information: no need to query the database schema.
GRDBPrecondition(originColumns.count == destinationColumns.count, "Number of columns don't match")
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return ForeignKeyInfo(destinationTable: destinationTable, mapping: mapping)
return mapping
}
// Incomplete information: let's look for schema foreign keys
@ -56,7 +57,7 @@ struct ForeignKeyRequest: Equatable {
if let foreignKey = foreignKeys.first {
if foreignKeys.count == 1 {
// Non-ambiguous
return foreignKey
return foreignKey.mapping
} else {
// Ambiguous: can't choose
fatalError("Ambiguous foreign key from \(originTable) to \(destinationTable)")
@ -70,13 +71,10 @@ struct ForeignKeyRequest: Equatable {
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return ForeignKeyInfo(destinationTable: destinationTable, mapping: mapping)
return mapping
}
}
fatalError("Could not infer foreign key from \(originTable) to \(destinationTable)")
}
}
/// The (origin, destination) column pairs that join a left table to a right table.
typealias ForeignKeyMapping = [(origin: String, destination: String)]

View File

@ -58,7 +58,7 @@
/// }
///
/// See ForeignKey for more information.
public struct HasManyAssociation<Origin, Destination>: Association {
public struct HasManyAssociation<Origin, Destination>: AssociationToMany {
/// :nodoc:
public typealias OriginRowDecoder = Origin
@ -66,108 +66,16 @@ public struct HasManyAssociation<Origin, Destination>: Association {
public typealias RowDecoder = Destination
/// :nodoc:
public var _impl: JoinAssociationImpl
public var sqlAssociation: SQLAssociation
/// :nodoc:
public init(_impl: JoinAssociationImpl) {
self._impl = _impl
public init(sqlAssociation: SQLAssociation) {
self.sqlAssociation = sqlAssociation
}
}
// Allow HasManyAssociation(...).filter(key: ...)
extension HasManyAssociation: TableRequest where Destination: TableRecord {
/// :nodoc:
public var databaseTableName: String { return Destination.databaseTableName }
}
extension HasManyAssociation where Origin: TableRecord, Destination: TableRecord {
private func makeAggregate(_ expression: SQLExpression) -> AssociationAggregate<Origin> {
return AssociationAggregate { request in
let tableAlias = TableAlias()
let request = request
.joining(optional: self.aliased(tableAlias))
.groupByPrimaryKey()
let expression = tableAlias[expression]
return (request: request, expression: expression)
}
}
/// The number of associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.count())
public var count: AssociationAggregate<Origin> {
return makeAggregate(SQLExpressionCountDistinct(Column.rowID)).aliased("\(key)Count")
}
/// An aggregate that is true if there exists no associated records.
///
/// For example:
///
/// Team.having(Team.players.isEmpty())
/// Team.having(!Team.players.isEmpty())
/// Team.having(Team.players.isEmpty() == false)
public var isEmpty: AssociationAggregate<Origin> {
return makeAggregate(SQLExpressionIsEmpty(SQLExpressionCountDistinct(Column.rowID)))
}
/// The average value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.average(Column("score")))
public func average(_ expression: SQLExpressible) -> AssociationAggregate<Origin> {
let aggregate = makeAggregate(SQLExpressionFunction(.avg, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("average\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The maximum value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.max(Column("score")))
public func max(_ expression: SQLExpressible) -> AssociationAggregate<Origin> {
let aggregate = makeAggregate(SQLExpressionFunction(.max, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("max\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The minimum value of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.min(Column("score")))
public func min(_ expression: SQLExpressible) -> AssociationAggregate<Origin> {
let aggregate = makeAggregate(SQLExpressionFunction(.min, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("min\(key.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// The sum of the given expression in associated records.
///
/// For example:
///
/// Team.annotated(with: Team.players.min(Column("score")))
public func sum(_ expression: SQLExpressible) -> AssociationAggregate<Origin> {
let aggregate = makeAggregate(SQLExpressionFunction(.sum, arguments: expression))
if let column = expression as? ColumnExpression {
return aggregate.aliased("\(key)\(column.name.uppercasingFirstCharacter)Sum")
} else {
return aggregate
}
}
}
extension HasManyAssociation: TableRequest where Destination: TableRecord { }
extension TableRecord {
/// Creates a "Has many" association between Self and the
@ -199,10 +107,10 @@ extension TableRecord {
/// print("\(authorship.author.name) wrote \(authorship.book.title)")
/// }
///
/// It is recommended that you define, alongside the association, a property
/// with the same name:
/// It is recommended that you define, alongside the static association, a
/// property with the same name:
///
/// struct Author: TableRecord {
/// struct Author: TableRecord, EncodableRecord {
/// static let books = hasMany(Book.self)
/// var books: QueryInterfaceRequest<Book> {
/// return request(for: Author.books)
@ -238,13 +146,13 @@ extension TableRecord {
destinationTable: databaseTableName,
foreignKey: foreignKey)
let joinCondition = JoinCondition(
let condition = SQLJoin.Condition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: false)
return HasManyAssociation(_impl: JoinAssociationImpl(
return HasManyAssociation(sqlAssociation: SQLAssociation(
key: key ?? Destination.databaseTableName,
joinCondition: joinCondition,
condition: condition,
relation: Destination.all().relation))
}
}

View File

@ -0,0 +1,135 @@
/// The **HasManyThrough** association is often used to set up a many-to-many
/// connection with another record. This association indicates that the
/// declaring record can be matched with zero or more instances of another
/// record by proceeding through a third record.
///
/// For example, consider the practice of passport delivery. One coutry
/// "has many" citizens "through" its passports:
///
/// struct Country: TableRecord {
/// static let passports = hasMany(Passport.self)
/// static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen)
/// ...
/// }
///
/// struct Passport: TableRecord {
/// static let citizen = belongsTo(Citizen.self)
/// ...
/// }
///
/// struct Citizen: TableRecord { ... }
///
/// The **HasManyThrough** association is also useful for setting up
/// "shortcuts" through nested HasMany associations. For example, if a document
/// has many sections, and a section has many paragraphs, you may sometimes want
/// to get a simple collection of all paragraphs in the document. You could set
/// that up this way:
///
/// struct Document: TableRecord {
/// static let sections = hasMany(Section.self)
/// static let paragraphs = hasMany(Paragraph.self, through: sections, using: Section.paragraphs)
/// }
///
/// struct Section: TableRecord {
/// static let paragraphs = hasMany(Paragraph.self)
/// }
///
/// struct Paragraph: TableRecord {
/// }
///
/// As in the examples above, **HasManyThrough** association is always built from
/// two other associations: the `through:` and `using:` arguments. Those
/// associations can be any other association (BelongsTo, HasMany,
/// HasManyThrough, etc).
public struct HasManyThroughAssociation<Origin, Destination>: AssociationToMany {
/// :nodoc:
public typealias OriginRowDecoder = Origin
/// :nodoc:
public typealias RowDecoder = Destination
/// :nodoc:
public var sqlAssociation: SQLAssociation
/// :nodoc:
public init(sqlAssociation: SQLAssociation) {
self.sqlAssociation = sqlAssociation
}
}
// Allow HasManyThroughAssociation(...).filter(key: ...)
extension HasManyThroughAssociation: TableRequest where Destination: TableRecord { }
extension TableRecord {
/// Creates a "Has Many Through" association between Self and the
/// destination type.
///
/// struct Country: TableRecord {
/// static let passports = hasMany(Passport.self)
/// static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen)
/// }
///
/// struct Passport: TableRecord {
/// static let citizen = belongsTo(Citizen.self)
/// }
///
/// struct Citizen: TableRecord { }
///
/// The association will let you define requests that load both the source
/// and the destination type:
///
/// // A request for all (country, citizen) pairs:
/// let request = Country.including(required: Coutry.citizens)
///
/// To consume those requests, define a type that adopts both the
/// FetchableRecord and Decodable protocols:
///
/// struct Citizenship: FetchableRecord, Decodable {
/// var country: Country
/// var citizen: Citizen
/// }
///
/// let citizenships = try dbQueue.read { db in
/// return try Citizenship.fetchAll(db, request)
/// }
/// for citizenship in citizenships {
/// print("\(citizenship.citizen.name) is a citizen of \(citizenship.country.name)")
/// }
///
/// It is recommended that you define, alongside the static association, a
/// property with the same name:
///
/// struct Country: TableRecord, EncodableRecord {
/// static let passports = hasMany(Passport.self)
/// static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen)
/// var citizens: QueryInterfaceRequest<Citizen> {
/// return request(for: Country.citizens)
/// }
/// }
///
/// This property will let you navigate from the source type to the
/// destination type:
///
/// try dbQueue.read { db in
/// let country: Country = ...
/// let citizens = try country.citizens.fetchAll(db) // [Country]
/// }
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - through: An association from Self to the intermediate type.
/// - using: An association from the intermediate type to the
/// destination type.
public static func hasMany<Pivot, Target>(
_ destination: Target.RowDecoder.Type,
through pivot: Pivot,
using target: Target)
-> HasManyThroughAssociation<Self, Target.RowDecoder>
where Pivot: Association,
Target: Association,
Pivot.OriginRowDecoder == Self,
Pivot.RowDecoder == Target.OriginRowDecoder
{
return HasManyThroughAssociation(sqlAssociation: target.sqlAssociation.appending(pivot.sqlAssociation))
}
}

View File

@ -60,7 +60,7 @@
/// }
///
/// See ForeignKey for more information.
public struct HasOneAssociation<Origin, Destination>: Association {
public struct HasOneAssociation<Origin, Destination>: AssociationToOne {
/// :nodoc:
public typealias OriginRowDecoder = Origin
@ -68,19 +68,16 @@ public struct HasOneAssociation<Origin, Destination>: Association {
public typealias RowDecoder = Destination
/// :nodoc:
public var _impl: JoinAssociationImpl
public var sqlAssociation: SQLAssociation
/// :nodoc:
public init(_impl: JoinAssociationImpl) {
self._impl = _impl
public init(sqlAssociation: SQLAssociation) {
self.sqlAssociation = sqlAssociation
}
}
// Allow HasOneAssociation(...).filter(key: ...)
extension HasOneAssociation: TableRequest where Destination: TableRecord {
/// :nodoc:
public var databaseTableName: String { return Destination.databaseTableName }
}
extension HasOneAssociation: TableRequest where Destination: TableRecord { }
extension TableRecord {
/// Creates a "Has one" association between Self and the
@ -112,10 +109,10 @@ extension TableRecord {
/// print("\(countryInfo.country.name) has \(countryInfo.demographics.population) citizens")
/// }
///
/// It is recommended that you define, alongside the association, a property
/// with the same name:
/// It is recommended that you define, alongside the static association, a
/// property with the same name:
///
/// struct Country: TableRecord {
/// struct Country: TableRecord, EncodableRecord {
/// static let demographics = hasOne(Demographics.self)
/// var demographics: QueryInterfaceRequest<Demographics> {
/// return request(for: Country.demographics)
@ -151,13 +148,13 @@ extension TableRecord {
destinationTable: databaseTableName,
foreignKey: foreignKey)
let joinCondition = JoinCondition(
let condition = SQLJoin.Condition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: false)
return HasOneAssociation(_impl: JoinAssociationImpl(
return HasOneAssociation(sqlAssociation: SQLAssociation(
key: key ?? Destination.databaseTableName,
joinCondition: joinCondition,
condition: condition,
relation: Destination.all().relation))
}
}

View File

@ -0,0 +1,115 @@
/// A **HasOneThrough** association sets up a one-to-one connection with
/// another record. This association indicates that the declaring record can be
/// matched with one instance of another record by proceeding through a third
/// record. For example, if each book belongs to a library, and each library has
/// one address, then one knows where the book should be returned to:
///
/// struct Book: TableRecord {
/// static let library = belongsTo(Library.self)
/// static let returnAddress = hasOne(Address.self, through: library, using: library.address)
/// ...
/// }
///
/// struct Library: TableRecord {
/// static let address = hasOne(Address.self)
/// ...
/// }
///
/// struct Address: TableRecord { ... }
///
/// As in the example above, **HasOneThrough** association is always built from
/// two other associations: the `through:` and `using:` arguments. Those
/// associations can be any other association to one (BelongsTo, HasOne,
/// HasOneThrough).
public struct HasOneThroughAssociation<Origin, Destination>: AssociationToOne {
/// :nodoc:
public typealias OriginRowDecoder = Origin
/// :nodoc:
public typealias RowDecoder = Destination
/// :nodoc:
public var sqlAssociation: SQLAssociation
/// :nodoc:
public init(sqlAssociation: SQLAssociation) {
self.sqlAssociation = sqlAssociation
}
}
// Allow HasOneThroughAssociation(...).filter(key: ...)
extension HasOneThroughAssociation: TableRequest where Destination: TableRecord { }
extension TableRecord {
/// Creates a "Has One Through" association between Self and the
/// destination type.
///
/// struct Book: TableRecord {
/// static let library = belongsTo(Library.self)
/// static let returnAddress = hasOne(Address.self, through: library, using: library.address)
/// }
///
/// struct Library: TableRecord {
/// static let address = hasOne(Address.self)
/// }
///
/// struct Address: TableRecord { ... }
///
/// The association will let you define requests that load both the source
/// and the destination type:
///
/// // A request for all (book, returnAddress) pairs:
/// let request = Book.including(required: Book.returnAddress)
///
/// To consume those requests, define a type that adopts both the
/// FetchableRecord and Decodable protocols:
///
/// struct Todo: FetchableRecord, Decodable {
/// var book: Book
/// var address: Address
/// }
///
/// let todos = try dbQueue.read { db in
/// return try Todo.fetchAll(db, request)
/// }
/// for todo in todos {
/// print("Please return \(todo.book) to \(todo.address)")
/// }
///
/// It is recommended that you define, alongside the static association, a
/// property with the same name:
///
/// struct Book: TableRecord, EncodableRecord {
/// static let library = belongsTo(Library.self)
/// static let returnAddress = hasOne(Address.self, through: library, using: library.address)
/// var returnAddress: QueryInterfaceRequest<Address> {
/// return request(for: Book.returnAddress)
/// }
/// }
///
/// This property will let you navigate from the source type to the
/// destination type:
///
/// try dbQueue.read { db in
/// let book: Book = ...
/// let address = try book.returnAddress.fetchOne(db) // Address?
/// }
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - through: An association from Self to the intermediate type.
/// - using: An association from the intermediate type to the
/// destination type.
public static func hasOne<Pivot, Target>(
_ destination: Target.RowDecoder.Type,
through pivot: Pivot,
using target: Target)
-> HasOneThroughAssociation<Self, Target.RowDecoder>
where Pivot: AssociationToOne,
Target: AssociationToOne,
Pivot.OriginRowDecoder == Self,
Pivot.RowDecoder == Target.OriginRowDecoder
{
return HasOneThroughAssociation(sqlAssociation: target.sqlAssociation.appending(pivot.sqlAssociation))
}
}

View File

@ -42,7 +42,6 @@ struct DatabasePromise<T> {
}
}
// TODO: write human-readable documentation for this classic monadic operation
func flatMap<U>(_ transform: @escaping (T) -> DatabasePromise<U>) -> DatabasePromise<U> {
return DatabasePromise<U> { db in
try transform(self.resolve(db)).resolve(db)

View File

@ -5,28 +5,44 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
/// associated record are selected. The returned association does not
/// require that the associated database table contains a matching row.
public func including<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
return mapQuery { $0.mapRelation { association._impl.joinedRelation($0, joinOperator: .optional) } }
return mapQuery {
$0.mapRelation {
association.sqlAssociation.relation(from: $0, required: false)
}
}
}
/// Creates a request that includes an association. The columns of the
/// associated record are selected. The returned association requires
/// that the associated database table contains a matching row.
public func including<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
return mapQuery { $0.mapRelation { association._impl.joinedRelation($0, joinOperator: .required) } }
return mapQuery {
$0.mapRelation {
association.sqlAssociation.relation(from: $0, required: true)
}
}
}
/// Creates a request that includes an association. The columns of the
/// associated record are not selected. The returned association does not
/// require that the associated database table contains a matching row.
public func joining<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
return mapQuery { $0.mapRelation { association.select([])._impl.joinedRelation($0, joinOperator: .optional) } }
return mapQuery {
$0.mapRelation {
association.select([]).sqlAssociation.relation(from: $0, required: false)
}
}
}
/// Creates a request that includes an association. The columns of the
/// associated record are not selected. The returned association requires
/// that the associated database table contains a matching row.
public func joining<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
return mapQuery { $0.mapRelation { association.select([])._impl.joinedRelation($0, joinOperator: .required) } }
return mapQuery {
$0.mapRelation {
association.select([]).sqlAssociation.relation(from: $0, required: true)
}
}
}
// MARK: - Association Aggregates
@ -76,12 +92,12 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
}
}
extension MutablePersistableRecord {
extension TableRecord where Self: EncodableRecord {
/// Creates a request that fetches the associated record(s).
///
/// For example:
///
/// struct Team: {
/// struct Team: TableRecord, EncodableRecord {
/// static let players = hasMany(Player.self)
/// var players: QueryInterfaceRequest<Player> {
/// return request(for: Team.players)
@ -91,7 +107,10 @@ extension MutablePersistableRecord {
/// let team: Team = ...
/// let players = try team.players.fetchAll(db) // [Player]
public func request<A: Association>(for association: A) -> QueryInterfaceRequest<A.RowDecoder> where A.OriginRowDecoder == Self {
return association.request(from: self)
let relation = association.sqlAssociation.relation(
to: type(of: self).databaseTableName,
container: { try PersistenceContainer($0, self) })
return QueryInterfaceRequest<A.RowDecoder>(query: SQLSelectQuery(relation: relation))
}
}

View File

@ -1,24 +1,32 @@
/// QueryInterfaceRequest is the type of requests generated by TableRecord:
// QueryInterfaceRequest is the type of requests generated by TableRecord:
//
// struct Player: TableRecord { ... }
// let playerRequest = Player.all() // QueryInterfaceRequest<Player>
//
// It wraps an SQLSelectQuery, and has an attached type.
//
// The attached type helps decoding raw database values:
//
// try dbQueue.read { db in
// try playerRequest.fetchAll(db) // [Player]
// }
//
// The attached type also helps the compiler validate associated requests:
//
// playerRequest.including(required: Player.team) // OK
// fruitRequest.including(required: Player.team) // Does not compile
/// QueryInterfaceRequest is a request that generates SQL for you.
///
/// struct Player: TableRecord { ... }
/// let playerRequest = Player.all() // QueryInterfaceRequest<Player>
///
/// It wraps an SQLSelectQuery, and has an attached type.
///
/// The attached type helps decoding raw database values:
/// For example:
///
/// try dbQueue.read { db in
/// try playerRequest.fetchAll(db) // [Player]
/// try playerRequest.asRequest(of: Row.self).fetchAll(db) // [Row]
/// let request = Player
/// .filter(Column("score") > 1000)
/// .order(Column("name"))
/// let players = try request.fetchAll(db) // [Player]
/// }
///
/// The attached type also helps the compiler validate associated requests:
///
/// playerRequest.including(required: Player.team) // OK
/// fruitRequest.including(required: Player.team) // Does not compile
/// A QueryInterfaceRequest describes an SQL query.
///
/// See https://github.com/groue/GRDB.swift#the-query-interface
public struct QueryInterfaceRequest<T> {
var query: SQLSelectQuery
@ -106,8 +114,39 @@ extension QueryInterfaceRequest : DerivableRequest, AggregatingRequest {
/// let request = Player.all().select(sql: "max(score)", as: Int.self)
/// let maxScore: Int? = try request.fetchOne(db)
/// }
public func select<RowDecoder>(sql: String, arguments: StatementArguments? = nil, as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return select(SQLSelectionLiteral(sql, arguments: arguments), as: type)
public func select<RowDecoder>(sql: String, arguments: StatementArguments = StatementArguments(), as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return select(literal: SQLLiteral(sql: sql, arguments: arguments), as: type)
}
/// Creates a request which selects an SQL *literal*, and fetches values of
/// type *type*.
///
/// try dbQueue.read { db in
/// // SELECT IFNULL(name, 'Anonymous') FROM player WHERE id = 42
/// let request = Player.
/// .filter(primaryKey: 42)
/// .select(
/// SQLLiteral(
/// sql: "IFNULL(name, ?)",
/// arguments: ["Anonymous"]),
/// as: String.self)
/// let name: String? = try request.fetchOne(db)
/// }
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// try dbQueue.read { db in
/// // SELECT IFNULL(name, 'Anonymous') FROM player WHERE id = 42
/// let request = Player.
/// .filter(primaryKey: 42)
/// .select(
/// literal: "IFNULL(name, \("Anonymous"))",
/// as: String.self)
/// let name: String? = try request.fetchOne(db)
/// }
public func select<RowDecoder>(literal sqlLiteral: SQLLiteral, as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return select(SQLSelectionLiteral(literal: sqlLiteral), as: type)
}
/// Creates a request which appends *selection*.
@ -340,8 +379,16 @@ extension TableRecord {
///
/// // SELECT id, email FROM player
/// let request = Player.select(sql: "id, email")
public static func select(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> {
return all().select(sql: sql, arguments: arguments)
public static func select(sql: String, arguments: StatementArguments = StatementArguments()) -> QueryInterfaceRequest<Self> {
return select(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request which selects an SQL *literal*.
///
/// // SELECT id, email FROM player
/// let request = Player.select(literal: SQLLiteral(sql: "id, email"))
public static func select(literal sqlLiteral: SQLLiteral) -> QueryInterfaceRequest<Self> {
return all().select(literal: sqlLiteral)
}
/// Creates a request which selects *selection*, and fetches values of
@ -376,10 +423,22 @@ extension TableRecord {
/// let request = Player.select(sql: "max(score)", as: Int.self)
/// let maxScore: Int? = try request.fetchOne(db)
/// }
public static func select<RowDecoder>(sql: String, arguments: StatementArguments? = nil, as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return all().select(sql: sql, arguments: arguments, as: type)
public static func select<RowDecoder>(sql: String, arguments: StatementArguments = StatementArguments(), as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return all().select(literal: SQLLiteral(sql: sql, arguments: arguments), as: type)
}
/// Creates a request which selects an SQL *literal*, and fetches values of
/// type *type*.
///
/// try dbQueue.read { db in
/// // SELECT max(score) FROM player
/// let request = Player.select(literal: SQLLiteral(sql: "max(score)"), as: Int.self)
/// let maxScore: Int? = try request.fetchOne(db)
/// }
public static func select<RowDecoder>(literal sqlLiteral: SQLLiteral, as type: RowDecoder.Type) -> QueryInterfaceRequest<RowDecoder> {
return all().select(literal: sqlLiteral, as: type)
}
/// Creates a request with the provided *predicate*.
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
@ -454,10 +513,28 @@ extension TableRecord {
/// The selection defaults to all columns. This default can be changed for
/// all requests by the `TableRecord.databaseSelection` property, or
/// for individual requests with the `TableRecord.select` method.
public static func filter(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> {
return all().filter(sql: sql, arguments: arguments)
public static func filter(sql: String, arguments: StatementArguments = StatementArguments()) -> QueryInterfaceRequest<Self> {
return filter(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request with the provided *predicate*.
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// let request = Player.filter(literal: SQLLiteral(sql: "email = ?", arguments: ["arthur@example.com"]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// let request = Player.filter(literal: "name = \("O'Brien"))
///
/// The selection defaults to all columns. This default can be changed for
/// all requests by the `TableRecord.databaseSelection` property, or
/// for individual requests with the `TableRecord.select` method.
public static func filter(literal sqlLiteral: SQLLiteral) -> QueryInterfaceRequest<Self> {
// NOT TESTED
return all().filter(literal: sqlLiteral)
}
/// Creates a request sorted according to the
/// provided *orderings*.
///
@ -507,10 +584,28 @@ extension TableRecord {
/// The selection defaults to all columns. This default can be changed for
/// all requests by the `TableRecord.databaseSelection` property, or
/// for individual requests with the `TableRecord.select` method.
public static func order(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> {
return all().order(sql: sql, arguments: arguments)
public static func order(sql: String, arguments: StatementArguments = StatementArguments()) -> QueryInterfaceRequest<Self> {
return all().order(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request sorted according to an SQL *literal*.
///
/// // SELECT * FROM player ORDER BY name
/// let request = Player.order(literal: SQLLiteral(sql: "name"))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// // SELECT * FROM player ORDER BY name
/// let request = Player.order(literal: "name"))
///
/// The selection defaults to all columns. This default can be changed for
/// all requests by the `TableRecord.databaseSelection` property, or
/// for individual requests with the `TableRecord.select` method.
public static func order(literal sqlLiteral: SQLLiteral) -> QueryInterfaceRequest<Self> {
return all().order(literal: sqlLiteral)
}
/// Creates a request which fetches *limit* rows, starting at
/// *offset*.
///

View File

@ -51,8 +51,38 @@ extension SelectionRequest {
/// request
/// .select(sql: "id")
/// .select(sql: "email")
public func select(sql: String, arguments: StatementArguments? = nil) -> Self {
return select(SQLSelectionLiteral(sql, arguments: arguments))
public func select(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
return select(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request which selects an SQL *literal*.
///
/// // SELECT id, email, score + 1000 FROM player
/// let bonus = 1000
/// var request = Player.all()
/// request = request.select(literal: SQLLiteral(sql: """
/// id, email, score + ?
/// """, arguments: [bonus]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// // SELECT id, email, score + 1000 FROM player
/// let bonus = 1000
/// var request = Player.all()
/// request = request.select(literal: """
/// id, email, score + \(bonus)
/// """)
///
/// Any previous selection is replaced:
///
/// // SELECT email FROM player
/// request
/// .select(...)
/// .select(literal: SQLLiteral(sql: "email"))
public func select(literal sqlLiteral: SQLLiteral) -> Self {
// NOT TESTED
return select(SQLSelectionLiteral(literal: sqlLiteral))
}
}
@ -91,10 +121,29 @@ extension FilteredRequest {
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// var request = Player.all()
/// request = request.filter(sql: "email = ?", arguments: ["arthur@example.com"])
public func filter(sql: String, arguments: StatementArguments? = nil) -> Self {
return filter(SQLExpressionLiteral(sql, arguments: arguments))
public func filter(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
return filter(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request with the provided *predicate* added to the
/// eventual set of already applied predicates.
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// var request = Player.all()
/// request = request.filter(literal: SQLLiteral(sql: """
/// email = ?
/// """, arguments: ["arthur@example.com"])
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// var request = Player.all()
/// request = request.filter(literal: "name = \("O'Brien")")
public func filter(literal sqlLiteral: SQLLiteral) -> Self {
// NOT TESTED
return filter(SQLExpressionLiteral(literal: sqlLiteral))
}
/// Creates a request that matches nothing.
///
/// // SELECT * FROM player WHERE 0
@ -205,8 +254,8 @@ extension TableRequest where Self: FilteredRequest {
return key
// Preserve ordering of columns in the unique index
.sorted { (kv1, kv2) in
let index1 = lowercaseColumns.index(of: kv1.key.lowercased())!
let index2 = lowercaseColumns.index(of: kv2.key.lowercased())!
let index1 = lowercaseColumns.firstIndex(of: kv1.key.lowercased())!
let index2 = lowercaseColumns.firstIndex(of: kv2.key.lowercased())!
return index1 < index2
}
.map { (column, value) in Column(column) == value }
@ -268,24 +317,35 @@ extension AggregatingRequest {
}
/// Creates a request with a new grouping.
public func group(sql: String, arguments: StatementArguments? = nil) -> Self {
public func group(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
return group(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request with a new grouping.
public func group(literal sqlLiteral: SQLLiteral) -> Self {
// NOT TESTED
// This "expression" is not a real expression. We support raw sql which
// actually contains several expressions:
//
// request = Player.group(sql: "teamId, level")
//
// This is why we use the "unsafe" flag, so that the SQLExpressionLiteral
// does not output its safe wrapping parenthesis, and generates
// invalid SQL.
var expression = SQLExpressionLiteral(sql, arguments: arguments)
expression.unsafeRaw = true
return group(expression)
// This is why we use the "unsafeLiteral" initializer, so that the
// SQLExpressionLiteral does not wrap input in parentheses, and
// generates invalid SQL `GROUP BY (teamId, level)`.
return group(SQLExpressionLiteral(unsafeLiteral: sqlLiteral))
}
/// Creates a request with the provided *sql* added to the
/// eventual set of already applied predicates.
public func having(sql: String, arguments: StatementArguments? = nil) -> Self {
return having(SQLExpressionLiteral(sql, arguments: arguments))
public func having(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
return having(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request with the provided *sql* added to the
/// eventual set of already applied predicates.
public func having(literal sqlLiteral: SQLLiteral) -> Self {
// NOT TESTED
return having(SQLExpressionLiteral(literal: sqlLiteral))
}
}
@ -362,7 +422,7 @@ extension OrderedRequest {
return order { _ in orderings }
}
/// Creates a request with the provided *sql* used for sorting.
/// Creates a request sorted according to *sql*.
///
/// // SELECT * FROM player ORDER BY name
/// var request = Player.all()
@ -374,18 +434,33 @@ extension OrderedRequest {
/// request
/// .order(sql: "email")
/// .order(sql: "name")
public func order(sql: String, arguments: StatementArguments? = nil) -> Self {
public func order(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
return order(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// Creates a request sorted according to an SQL *literal*.
///
/// // SELECT * FROM player ORDER BY name
/// var request = Player.all()
/// request = request.order(sql: "name")
///
/// Any previous ordering is replaced:
///
/// // SELECT * FROM player ORDER BY name
/// request
/// .order(sql: "email")
/// .order(sql: "name")
public func order(literal sqlLiteral: SQLLiteral) -> Self {
// NOT TESTED
// This "expression" is not a real expression. We support raw sql which
// actually contains several expressions:
//
// request = Player.order(sql: "teamId, level")
//
// This is why we use the "unsafe" flag, so that the SQLExpressionLiteral
// does not output its safe wrapping parenthesis, and generates
// invalid SQL.
var expression = SQLExpressionLiteral(sql, arguments: arguments)
expression.unsafeRaw = true
return order([expression])
// This is why we use the "unsafeLiteral" initializer, so that the
// SQLExpressionLiteral does not wrap input in parentheses, and
// generates invalid SQL `ORDER BY (teamId, level)`.
return order(SQLExpressionLiteral(unsafeLiteral: sqlLiteral))
}
}

View File

@ -1,22 +1,21 @@
// MARK: - SQLExpression
extension SQLExpression {
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// Converts an expression to an SQLExpressionLiteral
/// Converts an expression to an SQLLiteral
///
/// :nodoc:
public var literal: SQLExpressionLiteral {
public var sqlLiteral: SQLLiteral {
var context = SQLGenerationContext.literalGenerationContext(withArguments: true)
let sql = expressionSQL(&context)
return SQLExpressionLiteral(sql, arguments: context.arguments)
return SQLLiteral(sql: sql, arguments: context.arguments!)
}
/// The expression as a quoted SQL literal (not public in order to avoid abuses)
///
/// "foo'bar".databaseValue.sql // "'foo''bar'""
var sql: String {
/// "foo'bar".databaseValue.quotedSQL() // "'foo''bar'""
func quotedSQL() -> String {
var context = SQLGenerationContext.literalGenerationContext(withArguments: false)
return expressionSQL(&context)
}
@ -28,57 +27,61 @@ extension SQLExpression {
///
/// SQLExpressionLiteral is an expression built from a raw SQL snippet.
///
/// SQLExpressionLiteral("1 + 2")
/// SQLExpressionLiteral(sql: "1 + 2")
///
/// The SQL literal may contain `?` and colon-prefixed arguments:
///
/// SQLExpressionLiteral("? + ?", arguments: [1, 2])
/// SQLExpressionLiteral(":one + :two", arguments: ["one": 1, "two": 2])
/// SQLExpressionLiteral(sql: "? + ?", arguments: [1, 2])
/// SQLExpressionLiteral(sql: ":one + :two", arguments: ["one": 1, "two": 2])
public struct SQLExpressionLiteral : SQLExpression {
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// The SQL literal
public let sql: String
private let sqlLiteral: SQLLiteral
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// Eventual arguments that feed the `?` and colon-prefixed arguments in the
/// SQL literal
public let arguments: StatementArguments?
/// If safe, an SQLExpressionLiteral("foo") wraps itself in parenthesis,
/// and outputs "(foo)" in SQL queries. This avoids any bug due to operator
/// precedence. When unsafe, the expression literal does not wrap itself
/// in parenthesis and outputs its raw sql.
var unsafeRaw: Bool = false
public var sql: String { return sqlLiteral.sql }
public var arguments: StatementArguments { return sqlLiteral.arguments }
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// Creates an SQL literal expression.
///
/// SQLExpressionLiteral("1 + 2")
/// SQLExpressionLiteral("? + ?", arguments: [1, 2])
/// SQLExpressionLiteral(":one + :two", arguments: ["one": 1, "two": 2])
public init(_ sql: String, arguments: StatementArguments? = nil) {
self.sql = sql
self.arguments = arguments
/// SQLExpressionLiteral(sql: "1 + 2")
/// SQLExpressionLiteral(sql: "? + ?", arguments: [1, 2])
/// SQLExpressionLiteral(sql: ":one + :two", arguments: ["one": 1, "two": 2])
public init(sql: String, arguments: StatementArguments = StatementArguments()) {
self.init(literal: SQLLiteral(sql: sql, arguments: arguments))
}
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// Creates an SQL literal expression.
///
/// SQLExpressionLiteral(literal: SQLLiteral(sql: "1 + 2")
/// SQLExpressionLiteral(literal: SQLLiteral(sql: "? + ?", arguments: [1, 2]))
/// SQLExpressionLiteral(literal: SQLLiteral(sql: ":one + :two", arguments: ["one": 1, "two": 2]))
///
/// With Swift 5, you can safely embed raw values in your SQL queries,
/// without any risk of syntax errors or SQL injection:
///
/// SQLExpressionLiteral(literal: "\(1) + \(2)")
public init(literal sqlLiteral: SQLLiteral) {
self.init(unsafeLiteral: sqlLiteral.mapSQL { "(\($0))" })
}
/// Creates an SQL literal expression without wrapping the SQL literal
/// inside parentheses. It is unsafe because the result expression can not
/// be safely composed with other expressions.
init(unsafeLiteral sqlLiteral: SQLLiteral) {
self.sqlLiteral = sqlLiteral
}
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
/// :nodoc:
public func expressionSQL(_ context: inout SQLGenerationContext) -> String {
if let arguments = arguments {
if context.appendArguments(arguments) == false {
// GRDB limitation: we don't know how to look for `?` in sql and
// replace them with with literals.
fatalError("Not implemented")
}
}
if unsafeRaw {
return sql
} else {
return "(" + sql + ")"
if context.append(arguments: sqlLiteral.arguments) == false {
// GRDB limitation: we don't know how to look for `?` in sql and
// replace them with with literals.
fatalError("Not implemented")
}
return sqlLiteral.sql
}
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
@ -212,18 +215,6 @@ public struct SQLBinaryOperator : Hashable {
}
return SQLBinaryOperator(negatedSQL, negated: sql)
}
#if !swift(>=4.2)
/// :nodoc:
public var hashValue: Int {
return sql.hashValue ^ (negatedSQL?.hashValue ?? 0)
}
/// :nodoc:
public static func == (lhs: SQLBinaryOperator, rhs: SQLBinaryOperator) -> Bool {
return lhs.sql == rhs.sql && lhs.negatedSQL == rhs.negatedSQL
}
#endif
}
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)

View File

@ -5,11 +5,12 @@
///
/// :nodoc:
public struct SQLGenerationContext {
private(set) var arguments: StatementArguments?
var arguments: StatementArguments?
private var resolvedNames: [TableAlias: String]
private var qualifierNeeded: Bool
/// Used for SQLExpression -> SQLExpressionLiteral conversion
/// and SQLInterpolation
static func literalGenerationContext(withArguments: Bool) -> SQLGenerationContext {
return SQLGenerationContext(
arguments: withArguments ? [] : nil,
@ -32,7 +33,7 @@ public struct SQLGenerationContext {
}
/// Used for TableRecord.selectionSQL
static func recordSelectionGenerationContext(alias: TableAlias) -> SQLGenerationContext {
static func recordSelectionGenerationContext() -> SQLGenerationContext {
return SQLGenerationContext(
arguments: nil,
resolvedNames: [:],
@ -40,7 +41,10 @@ public struct SQLGenerationContext {
}
/// Returns whether arguments could be appended
mutating func appendArguments(_ newArguments: StatementArguments) -> Bool {
mutating func append(arguments newArguments: StatementArguments) -> Bool {
if newArguments.isEmpty {
return true
}
guard let arguments = arguments else {
return false
}
@ -271,17 +275,10 @@ public class TableAlias: Hashable {
return expression.qualifiedExpression(with: self)
}
#if swift(>=4.2)
/// :nodoc:
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(root))
}
#else
/// :nodoc:
public var hashValue: Int {
return ObjectIdentifier(root).hashValue
}
#endif
/// :nodoc:
public static func == (lhs: TableAlias, rhs: TableAlias) -> Bool {
@ -312,7 +309,7 @@ extension Array where Element == TableAlias {
for (lowercaseName, group) in groups {
if group.count > 1 {
// It is a programmer error to reuse the same alias for multiple tables
GRDBPrecondition(group.filter({ $0.hasUserName }).count < 2, "ambiguous alias: \(group[0].identityName)")
GRDBPrecondition(group.count { $0.hasUserName } < 2, "ambiguous alias: \(group[0].identityName)")
ambiguousGroups.append(group)
} else {
uniqueLowercaseNames.insert(lowercaseName)
@ -341,7 +338,7 @@ extension Array where Element == TableAlias {
extension String {
/// "bar" => "bar"
/// "foo12" => "foo"
var databaseQualifierRadical: String {
fileprivate var databaseQualifierRadical: String {
let digits: ClosedRange<Character> = "0"..."9"
let radicalEndIndex = self // "foo12"
.reversed() // "21oof"

View File

@ -0,0 +1,140 @@
#if swift(>=5.0)
/// :nodoc:
extension SQLInterpolation {
/// Appends the table name of the record type.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = "SELECT * FROM \(Player.self)"
public mutating func appendInterpolation<T: TableRecord>(_ table: T.Type) {
sql += table.databaseTableName.quotedDatabaseIdentifier
}
/// Appends the selectable SQL.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = """
/// SELECT \(AllColumns()) FROM player
/// """
public mutating func appendInterpolation(_ selection: SQLSelectable) {
sql += selection.resultColumnSQL(&context)
}
/// Appends the expression SQL.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = """
/// SELECT \(Column("name")) FROM player
/// """
public mutating func appendInterpolation(_ expressible: SQLExpressible & SQLSelectable & SQLOrderingTerm) {
sql += expressible.sqlExpression.expressionSQL(&context)
}
/// Appends the name of the coding key.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = "
/// SELECT \(CodingKey.name) FROM player
/// """
public mutating func appendInterpolation(_ codingKey: SQLExpressible & SQLSelectable & SQLOrderingTerm & CodingKey) {
sql += codingKey.sqlExpression.expressionSQL(&context)
}
/// Appends the expression SQL, or NULL if it is nil.
///
/// // SELECT score + ? FROM player
/// let bonus = 1000
/// let request: SQLRequest<Int> = """
/// SELECT score + \(bonus) FROM player
/// """
public mutating func appendInterpolation<T: SQLExpressible>(_ expressible: T?) {
if let expressible = expressible {
sql += expressible.sqlExpression.expressionSQL(&context)
} else {
sql += "NULL"
}
}
/// Appends the name of the coding key.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = """
/// SELECT \(CodingKey.name) FROM player
/// """
public mutating func appendInterpolation(_ codingKey: CodingKey) {
appendInterpolation(Column(codingKey.stringValue))
}
/// Appends a sequence of expressions, wrapped in parentheses.
///
/// // SELECT * FROM player WHERE id IN (?,?,?)
/// let ids = [1, 2, 3]
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE id IN \(ids)
/// """
///
/// If the sequence is empty, an empty subquery is appended:
///
/// // SELECT * FROM player WHERE id IN (SELECT NULL WHERE NULL)
/// let ids: [Int] = []
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE id IN \(ids)
/// """
public mutating func appendInterpolation<S>(_ sequence: S) where S: Sequence, S.Element: SQLExpressible {
appendInterpolation(sequence.lazy.map { $0.sqlExpression })
}
/// Appends a sequence of expressions, wrapped in parentheses.
///
/// // SELECT * FROM player WHERE a IN (b, c + 2)
/// let expressions = [Column("b"), Column("c") + 2]
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE a IN \(expressions)
/// """
///
/// If the sequence is empty, an empty subquery is appended:
///
/// // SELECT * FROM player WHERE a IN (SELECT NULL WHERE NULL)
/// let expressions: [SQLExpression] = []
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE a IN \(expressions)
/// """
public mutating func appendInterpolation<S>(_ sequence: S) where S: Sequence, S.Element == SQLExpression {
sql += "("
var first = true
for element in sequence {
if first {
first = false
} else {
sql += ","
}
appendInterpolation(element)
}
if first {
sql += "SELECT NULL WHERE NULL"
}
sql += ")"
}
/// Appends the ordering SQL.
///
/// // SELECT name FROM player ORDER BY name DESC
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player ORDER BY \(Column("name").desc)
/// """
public mutating func appendInterpolation(_ ordering: SQLOrderingTerm) {
sql += ordering.orderingTermSQL(&context)
}
/// Appends the request SQL, wrapped in parentheses
///
/// // SELECT name FROM player WHERE score = (SELECT MAX(score) FROM player)
/// let subQuery: SQLRequest<Int> = "SELECT MAX(score) FROM player"
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE score = \(subQuery)
/// """
public mutating func appendInterpolation<T>(_ request: SQLRequest<T>) {
sql += "(" + request.sql + ")"
arguments += request.arguments
}
}
#endif

View File

@ -1,21 +1,13 @@
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
///
/// A "relation" as defined by the [relational terminology](https://en.wikipedia.org/wiki/Relational_database#Terminology):
///
/// > A set of tuples sharing the same attributes; a set of columns and rows.
///
/// :nodoc:
public /* TODO: internal */ struct SQLRelation {
struct SQLRelation {
var source: SQLSource
var selection: [SQLSelectable]
var filterPromise: DatabasePromise<SQLExpression?>
var ordering: SQLRelation.Ordering
var joins: OrderedDictionary<String, SQLJoin>
var alias: TableAlias? {
return source.alias
}
init(
source: SQLSource,
selection: [SQLSelectable] = [],
@ -101,15 +93,6 @@ enum SQLSource {
case table(tableName: String, alias: TableAlias?)
indirect case query(SQLSelectQuery)
var alias: TableAlias? {
switch self {
case .table(_, let alias):
return alias
case .query(let query):
return query.alias
}
}
func qualified(with alias: TableAlias) -> SQLSource {
switch self {
case .table(let tableName, let sourceAlias):
@ -217,134 +200,87 @@ extension SQLRelation {
// MARK: - SQLJoin
/// Not to be mismatched with SQL join operators (inner join, left join).
///
/// JoinOperator is designed to be hierarchically nested, unlike
/// SQL join operators.
///
/// Consider the following request for (A, B, C) tuples:
///
/// let r = A.including(optional: A.b.including(required: B.c))
///
/// It chains three associations, the first optional, the second required.
///
/// It looks like it means: "Give me all As, along with their Bs, granted those
/// Bs have their Cs. For As whose B has no C, give me a nil B".
///
/// It can not be expressed as one left join, and a regular join, as below,
/// Because this would not honor the first optional:
///
/// -- dubious
/// SELECT a.*, b.*, c.*
/// FROM a
/// LEFT JOIN b ON ...
/// JOIN c ON ...
///
/// Instead, it should:
/// - allow (A + missing (B + C))
/// - prevent (A + (B + missing C)).
///
/// This can be expressed in SQL with two left joins, and an extra condition:
///
/// -- likely correct
/// SELECT a.*, b.*, c.*
/// FROM a
/// LEFT JOIN b ON ...
/// LEFT JOIN c ON ...
/// WHERE NOT((b.id IS NOT NULL) AND (c.id IS NULL)) -- no B without C
///
/// This is currently not implemented, and requires a little more thought.
/// I don't even know if inventing a whole new way to perform joins should even
/// be on the table. But we have a hierarchical way to express joined queries,
/// and they have a meaning:
///
/// // what is my meaning?
/// A.including(optional: A.b.including(required: B.c))
///
/// :nodoc:
public /* TODO: internal */ enum JoinOperator {
case required, optional
}
/// The condition that links two joined tables.
///
/// Currently, we only support one kind of join condition: foreign keys.
///
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// <- the join condition -->
///
/// When we eventually add support for new ways to join tables, JoinCondition
/// is the type we'll need to update.
///
/// JoinCondition equality allows merging of associations:
///
/// // request1 and request2 are equivalent
/// let request1 = Book
/// .including(required: Book.author)
/// let request2 = Book
/// .including(required: Book.author)
/// .including(required: Book.author)
///
/// // request3 and request4 are equivalent
/// let request3 = Book
/// .including(required: Book.author.filter(condition1 && condition2))
/// let request4 = Book
/// .joining(required: Book.author.filter(condition1))
/// .including(optional: Book.author.filter(condition2))
///
/// :nodoc:
public /* TODO: internal */ struct JoinCondition: Equatable {
/// Definition of a foreign key
var foreignKeyRequest: ForeignKeyRequest
/// True if the table at the origin of the foreign key is on the left of
/// the sql JOIN operator.
struct SQLJoin {
/// The condition that links two joined tables.
///
/// Let's consider the `book.authorId -> author.id` foreign key.
/// Its origin table is `book`.
///
/// The origin table `book` is on the left of the JOIN operator for
/// the BelongsTo association:
///
/// -- Book.including(required: Book.author)
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
///
/// The origin table `book`is on the right of the JOIN operator for
/// the HasMany and HasOne associations:
///
/// -- Author.including(required: Author.books)
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
var originIsLeft: Bool
/// Returns an SQL expression for the join condition.
/// Currently, we only support one kind of join condition: foreign keys.
///
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// <- the SQL expression -->
/// <- the join condition -->
///
/// - parameter db: A database connection.
/// - parameter leftAlias: A TableAlias for the table on the left of the
/// JOIN operator.
/// - parameter rightAlias: A TableAlias for the table on the right of the
/// JOIN operator.
/// - Returns: An SQL expression.
func sqlExpression(_ db: Database, leftAlias: TableAlias, rightAlias: TableAlias) throws -> SQLExpression {
let foreignKeyMapping = try foreignKeyRequest.fetch(db).mapping
let columnMapping: [(left: Column, right: Column)]
if originIsLeft {
columnMapping = foreignKeyMapping.map { (left: Column($0.origin), right: Column($0.destination)) }
} else {
columnMapping = foreignKeyMapping.map { (left: Column($0.destination), right: Column($0.origin)) }
/// When we eventually add support for new ways to join tables, Condition
/// is the type we'll need to update.
///
/// Condition equality allows merging of associations:
///
/// // request1 and request2 are equivalent
/// let request1 = Book
/// .including(required: Book.author)
/// let request2 = Book
/// .including(required: Book.author)
/// .including(required: Book.author)
///
/// // request3 and request4 are equivalent
/// let request3 = Book
/// .including(required: Book.author.filter(condition1 && condition2))
/// let request4 = Book
/// .joining(required: Book.author.filter(condition1))
/// .including(optional: Book.author.filter(condition2))
struct Condition: Equatable {
/// Definition of a foreign key
var foreignKeyRequest: ForeignKeyRequest
/// True if the table at the origin of the foreign key is on the left of
/// the sql JOIN operator.
///
/// Let's consider the `book.authorId -> author.id` foreign key.
/// Its origin table is `book`.
///
/// The origin table `book` is on the left of the JOIN operator for
/// the BelongsTo association:
///
/// -- Book.including(required: Book.author)
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
///
/// The origin table `book`is on the right of the JOIN operator for
/// the HasMany and HasOne associations:
///
/// -- Author.including(required: Author.books)
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
var originIsLeft: Bool
var reversed: Condition {
return Condition(foreignKeyRequest: foreignKeyRequest, originIsLeft: !originIsLeft)
}
return columnMapping
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
.joined(operator: .and)
/// Returns an SQL expression for the join condition.
///
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// <- the SQL expression -->
///
/// - parameter db: A database connection.
/// - parameter leftAlias: A TableAlias for the table on the left of the
/// JOIN operator.
/// - parameter rightAlias: A TableAlias for the table on the right of the
/// JOIN operator.
/// - Returns: An SQL expression.
func sqlExpression(_ db: Database, leftAlias: TableAlias, rightAlias: TableAlias) throws -> SQLExpression {
let foreignKeyMapping = try foreignKeyRequest.fetchMapping(db)
let columnMapping: [(left: Column, right: Column)]
if originIsLeft {
columnMapping = foreignKeyMapping.map { (left: Column($0.origin), right: Column($0.destination)) }
} else {
columnMapping = foreignKeyMapping.map { (left: Column($0.destination), right: Column($0.origin)) }
}
return columnMapping
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
.joined(operator: .and)
}
}
}
struct SQLJoin {
var joinOperator: JoinOperator
var joinCondition: JoinCondition
var isRequired: Bool
var condition: Condition
var relation: SQLRelation
}
@ -445,7 +381,7 @@ extension SQLSource {
extension SQLJoin {
/// Returns nil if joins can't be merged (conflict in condition, relation...)
func merged(with other: SQLJoin) -> SQLJoin? {
guard joinCondition == other.joinCondition else {
guard condition == other.condition else {
// can't merge
return nil
}
@ -455,17 +391,9 @@ extension SQLJoin {
return nil
}
let mergedJoinOperator: JoinOperator
switch (joinOperator, other.joinOperator) {
case (.required, _), (_, .required):
mergedJoinOperator = .required
default:
mergedJoinOperator = .optional
}
return SQLJoin(
joinOperator: mergedJoinOperator,
joinCondition: joinCondition,
isRequired: isRequired || other.isRequired,
condition: condition,
relation: mergedRelation)
}
}

View File

@ -21,10 +21,6 @@ struct SQLSelectQuery {
self.havingExpression = havingExpression
self.limit = limit
}
var alias: TableAlias? {
return relation.alias
}
}
extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {

View File

@ -163,7 +163,7 @@ struct SQLSelectQueryGenerator {
}
}
let statement = try db.makeUpdateStatement(sql)
let statement = try db.makeUpdateStatement(sql: sql)
statement.arguments = context.arguments!
return statement
}
@ -179,7 +179,7 @@ struct SQLSelectQueryGenerator {
let sql = try self.sql(db, &context)
// Compile & set arguments
let statement = try db.makeSelectStatement(sql)
let statement = try db.makeSelectStatement(sql: sql)
statement.arguments = context.arguments! // not nil for this kind of context
return statement
}
@ -377,7 +377,7 @@ private enum SQLQualifiedSource {
return query.relation.allAliases
}
}
init(_ source: SQLSource) {
switch source {
case .table(let tableName, let alias):
@ -404,36 +404,36 @@ private enum SQLQualifiedSource {
/// A "qualified" join, where all tables are identified with a table alias.
private struct SQLQualifiedJoin {
private let joinOperator: JoinOperator
private let joinCondition: JoinCondition
private let isRequired: Bool
private let condition: SQLJoin.Condition
let relation: SQLQualifiedRelation
init(_ join: SQLJoin) {
self.joinOperator = join.joinOperator
self.joinCondition = join.joinCondition
self.isRequired = join.isRequired
self.condition = join.condition
self.relation = SQLQualifiedRelation(join.relation)
}
func sql(_ db: Database,_ context: inout SQLGenerationContext, leftAlias: TableAlias, isRequiredAllowed: Bool) throws -> String {
var isRequiredAllowed = isRequiredAllowed
var sql = ""
switch joinOperator {
case .optional:
isRequiredAllowed = false
sql += "LEFT JOIN"
case .required:
if isRequired {
guard isRequiredAllowed else {
// TODO: chainOptionalRequired
fatalError("Not implemented: chaining a required association behind an optional association")
}
sql += "JOIN"
} else {
isRequiredAllowed = false
sql += "LEFT JOIN"
}
sql += try " " + relation.source.sql(db, &context)
let rightAlias = relation.alias
let filters = try [
joinCondition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
condition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
relation.filterPromise.resolve(db)
].compactMap { $0 }
if !filters.isEmpty {

View File

@ -26,23 +26,19 @@ public protocol SQLSelectable {
// MARK: - SQLSelectionLiteral
struct SQLSelectionLiteral : SQLSelectable {
let sql: String
let arguments: StatementArguments?
private let sqlLiteral: SQLLiteral
init(_ sql: String, arguments: StatementArguments? = nil) {
self.sql = sql
self.arguments = arguments
init(literal sqlLiteral: SQLLiteral) {
self.sqlLiteral = sqlLiteral
}
func resultColumnSQL(_ context: inout SQLGenerationContext) -> String {
if let arguments = arguments {
if context.appendArguments(arguments) == false {
// GRDB limitation: we don't know how to look for `?` in sql and
// replace them with with literals.
fatalError("Not implemented")
}
if context.append(arguments: sqlLiteral.arguments) == false {
// GRDB limitation: we don't know how to look for `?` in sql and
// replace them with with literals.
fatalError("Not implemented")
}
return sql
return sqlLiteral.sql
}
func countedSQL(_ context: inout SQLGenerationContext) -> String {

View File

@ -187,7 +187,7 @@ extension SQLSpecificExpressible {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedCapitalized)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public var localizedCapitalized: SQLExpression {
return DatabaseFunction.localizedCapitalize.apply(sqlExpression)
}
@ -198,7 +198,7 @@ extension SQLSpecificExpressible {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedLowercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public var localizedLowercased: SQLExpression {
return DatabaseFunction.localizedLowercase.apply(sqlExpression)
}
@ -209,7 +209,7 @@ extension SQLSpecificExpressible {
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedUppercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
@available(iOS 9.0, OSX 10.11, watchOS 3.0, *)
@available(OSX 10.11, watchOS 3.0, *)
public var localizedUppercased: SQLExpression {
return DatabaseFunction.localizedUppercase.apply(sqlExpression)
}

View File

@ -29,7 +29,7 @@ extension Database {
let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: withoutRowID)
body(definition)
let sql = try definition.sql(self)
try execute(sql)
try execute(sql: sql)
}
#else
/// Creates a database table.
@ -54,14 +54,14 @@ extension Database {
/// - withoutRowID: If true, uses WITHOUT ROWID optimization.
/// - body: A closure that defines table columns and constraints.
/// - throws: A DatabaseError whenever an SQLite error occurs.
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
public func create(table name: String, temporary: Bool = false, ifNotExists: Bool = false, withoutRowID: Bool, body: (TableDefinition) -> Void) throws {
// WITHOUT ROWID was added in SQLite 3.8.2 http://www.sqlite.org/changes.html#version_3_8_2
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: withoutRowID)
body(definition)
let sql = try definition.sql(self)
try execute(sql)
try execute(sql: sql)
}
/// Creates a database table.
@ -88,7 +88,7 @@ extension Database {
let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: false)
body(definition)
let sql = try definition.sql(self)
try execute(sql)
try execute(sql: sql)
}
#endif
@ -98,7 +98,7 @@ extension Database {
///
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func rename(table name: String, to newName: String) throws {
try execute("ALTER TABLE \(name.quotedDatabaseIdentifier) RENAME TO \(newName.quotedDatabaseIdentifier)")
try execute(sql: "ALTER TABLE \(name.quotedDatabaseIdentifier) RENAME TO \(newName.quotedDatabaseIdentifier)")
}
/// Modifies a database table.
@ -117,7 +117,7 @@ extension Database {
let alteration = TableAlteration(name: name)
body(alteration)
let sql = try alteration.sql(self)
try execute(sql)
try execute(sql: sql)
}
/// Deletes a database table.
@ -126,7 +126,7 @@ extension Database {
///
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func drop(table name: String) throws {
try execute("DROP TABLE \(name.quotedDatabaseIdentifier)")
try execute(sql: "DROP TABLE \(name.quotedDatabaseIdentifier)")
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
@ -138,7 +138,7 @@ extension Database {
/// and use specific collations. To create such an index, use a raw SQL
/// query.
///
/// try db.execute("CREATE INDEX ...")
/// try db.execute(sql: "CREATE INDEX ...")
///
/// See https://www.sqlite.org/lang_createindex.html
///
@ -155,7 +155,7 @@ extension Database {
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: condition?.sqlExpression)
let sql = definition.sql()
try execute(sql)
try execute(sql: sql)
}
#else
/// Creates an index.
@ -166,7 +166,7 @@ extension Database {
/// and use specific collations. To create such an index, use a raw SQL
/// query.
///
/// try db.execute("CREATE INDEX ...")
/// try db.execute(sql: "CREATE INDEX ...")
///
/// See https://www.sqlite.org/lang_createindex.html
///
@ -181,7 +181,7 @@ extension Database {
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: nil)
let sql = definition.sql()
try execute(sql)
try execute(sql: sql)
}
/// Creates a partial index.
@ -198,13 +198,13 @@ extension Database {
/// - unique: If true, creates a unique index.
/// - ifNotExists: If false, no error is thrown if index already exists.
/// - condition: The condition that indexed rows must verify.
@available(iOS 8.2, OSX 10.10, *)
@available(OSX 10.10, *)
public func create(index name: String, on table: String, columns: [String], unique: Bool = false, ifNotExists: Bool = false, condition: SQLExpressible) throws {
// Partial indexes were introduced in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0
// It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: condition.sqlExpression)
let sql = definition.sql()
try execute(sql)
try execute(sql: sql)
}
#endif
@ -214,7 +214,7 @@ extension Database {
///
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func drop(index name: String) throws {
try execute("DROP INDEX \(name.quotedDatabaseIdentifier)")
try execute(sql: "DROP INDEX \(name.quotedDatabaseIdentifier)")
}
/// Delete and recreate from scratch all indices that use this collation.
@ -226,7 +226,7 @@ extension Database {
///
/// - throws: A DatabaseError whenever an SQLite error occurs.
public func reindex(collation: Database.CollationName) throws {
try execute("REINDEX \(collation.rawValue)")
try execute(sql: "REINDEX \(collation.rawValue)")
}
/// Delete and recreate from scratch all indices that use this collation.
@ -416,9 +416,9 @@ public final class TableDefinition {
///
/// - parameter sql: An SQL snippet
public func check(sql: String) {
var expression = SQLExpressionLiteral(sql)
expression.unsafeRaw = true // It's safe because this expression can't be composed with others
checkConstraints.append(expression)
// We do not want to wrap the SQL snippet inside parentheses around the
// checked SQL. This is why we use the "unsafeLiteral" initializer.
checkConstraints.append(SQLExpressionLiteral(unsafeLiteral: SQLLiteral(sql: sql)))
}
fileprivate func sql(_ db: Database) throws -> String {
@ -439,7 +439,7 @@ public final class TableDefinition {
let primaryKeyColumns: [String]
if let (columns, _) = primaryKeyConstraint {
primaryKeyColumns = columns
} else if let index = columns.index(where: { $0.primaryKey != nil }) {
} else if let index = columns.firstIndex(where: { $0.primaryKey != nil }) {
primaryKeyColumns = [columns[index].name]
} else {
// WITHOUT ROWID optimization requires a primary key. If the
@ -507,7 +507,7 @@ public final class TableDefinition {
for checkExpression in checkConstraints {
var chunks: [String] = []
chunks.append("CHECK")
chunks.append("(" + checkExpression.sql + ")")
chunks.append("(" + checkExpression.quotedSQL() + ")")
items.append(chunks.joined(separator: " "))
}
@ -721,9 +721,9 @@ public final class ColumnDefinition {
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func check(sql: String) -> Self {
var expression = SQLExpressionLiteral(sql)
expression.unsafeRaw = true // It's safe because this expression can't be composed with others
checkConstraints.append(expression)
// We do not want to wrap the SQL snippet inside parentheses around the
// checked SQL. This is why we use the "unsafeLiteral" initializer.
checkConstraints.append(SQLExpressionLiteral(unsafeLiteral: SQLLiteral(sql: sql)))
return self
}
@ -755,9 +755,9 @@ public final class ColumnDefinition {
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func defaults(sql: String) -> Self {
var expression = SQLExpressionLiteral(sql)
expression.unsafeRaw = true // It's safe because this expression can't be composed with others
defaultExpression = expression
// We do not want to wrap the SQL snippet inside parentheses around the
// checked SQL. This is why we use the "unsafeLiteral" initializer.
defaultExpression = SQLExpressionLiteral(unsafeLiteral: SQLLiteral(sql: sql))
return self
}
@ -861,12 +861,12 @@ public final class ColumnDefinition {
for checkConstraint in checkConstraints {
chunks.append("CHECK")
chunks.append("(" + checkConstraint.sql + ")")
chunks.append("(" + checkConstraint.quotedSQL() + ")")
}
if let defaultExpression = defaultExpression {
chunks.append("DEFAULT")
chunks.append(defaultExpression.sql)
chunks.append(defaultExpression.quotedSQL())
}
if let collationName = collationName {
@ -943,7 +943,7 @@ private struct IndexDefinition {
chunks.append("\(table.quotedDatabaseIdentifier)(\((columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))")
if let condition = condition {
chunks.append("WHERE")
chunks.append(condition.sql)
chunks.append(condition.quotedSQL())
}
return chunks.joined(separator: " ")
}

View File

@ -64,7 +64,7 @@ extension Database {
chunks.append("USING")
chunks.append(module)
let sql = chunks.joined(separator: " ")
try execute(sql)
try execute(sql: sql)
}
/// Creates a virtual database table.
@ -120,7 +120,7 @@ extension Database {
let sql = chunks.joined(separator: " ")
try inSavepoint {
try execute(sql)
try execute(sql: sql)
try module.database(self, didCreate: tableName, using: definition)
return .commit
}

View File

@ -1,9 +1,8 @@
import Foundation
extension MutablePersistableRecord where Self: Encodable {
extension EncodableRecord where Self: Encodable {
public func encode(to container: inout PersistenceContainer) {
let persistenceContainer = PersistenceContainer()
let encoder = RecordEncoder<Self>(persistenceContainer: persistenceContainer)
let encoder = RecordEncoder<Self>(persistenceContainer: container)
try! encode(to: encoder)
container = encoder.persistenceContainer
}
@ -12,7 +11,7 @@ extension MutablePersistableRecord where Self: Encodable {
// MARK: - RecordEncoder
/// The encoder that encodes a record into GRDB's PersistenceContainer
private class RecordEncoder<Record: MutablePersistableRecord>: Encoder {
private class RecordEncoder<Record: EncodableRecord>: Encoder {
var codingPath: [CodingKey] { return [] }
var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo }
private var _persistenceContainer: PersistenceContainer
@ -163,7 +162,7 @@ private class RecordEncoder<Record: MutablePersistableRecord>: Encoder {
// MARK: - ColumnEncoder
/// The encoder that encodes into a database column
private class ColumnEncoder<Record: MutablePersistableRecord>: Encoder {
private class ColumnEncoder<Record: EncodableRecord>: Encoder {
var recordEncoder: RecordEncoder<Record>
var key: CodingKey
var codingPath: [CodingKey] { return [key] }
@ -226,7 +225,7 @@ extension ColumnEncoder: SingleValueEncodingContainer {
private struct JSONRequiredError: Error { }
/// The encoder that always ends up with a JSONRequiredError
private struct JSONRequiredEncoder<Record: MutablePersistableRecord>: Encoder {
private struct JSONRequiredEncoder<Record: EncodableRecord>: Encoder {
var codingPath: [CodingKey]
var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo }

View File

@ -0,0 +1,387 @@
import Foundation // For JSONEncoder
/// Types that adopt EncodableRecord can be encoded into the database.
public protocol EncodableRecord {
/// Encodes the record into database values.
///
/// Store in the *container* argument all values that should be stored in
/// the columns of the database table (see databaseTableName()).
///
/// Primary key columns, if any, must be included.
///
/// struct Player: EncodableRecord {
/// var id: Int64?
/// var name: String?
///
/// func encode(to container: inout PersistenceContainer) {
/// container["id"] = id
/// container["name"] = name
/// }
/// }
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
func encode(to container: inout PersistenceContainer)
// MARK: - Customizing the Format of Database Columns
/// When the EncodableRecord type also adopts the standard Encodable
/// protocol, you can use this dictionary to customize the encoding process
/// into database rows.
///
/// For example:
///
/// // A key that holds a encoder's name
/// let encoderName = CodingUserInfoKey(rawValue: "encoderName")!
///
/// struct Player: PersistableRecord, Encodable {
/// // Customize the encoder name when encoding a database row
/// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"]
///
/// func encode(to encoder: Encoder) throws {
/// // Print the encoder name
/// print(encoder.userInfo[encoderName])
/// ...
/// }
/// }
///
/// let player = Player(...)
///
/// // prints "Database"
/// try player.insert(db)
///
/// // prints "JSON"
/// let encoder = JSONEncoder()
/// encoder.userInfo = [encoderName: "JSON"]
/// let data = try encoder.encode(player)
static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
/// When the EncodableRecord type also adopts the standard Encodable
/// protocol, this method controls the encoding process of nested properties
/// into JSON database columns.
///
/// The default implementation returns a JSONEncoder with the
/// following properties:
///
/// - dataEncodingStrategy: .base64
/// - dateEncodingStrategy: .millisecondsSince1970
/// - nonConformingFloatEncodingStrategy: .throw
/// - outputFormatting: .sortedKeys (iOS 11.0+, macOS 10.13+, watchOS 4.0+)
///
/// You can override those defaults:
///
/// struct Achievement: Encodable {
/// var name: String
/// var date: Date
/// }
///
/// struct Player: Encodable, PersistableRecord {
/// // stored in a JSON column
/// var achievements: [Achievement]
///
/// static func databaseJSONEncoder(for column: String) -> JSONEncoder {
/// let encoder = JSONEncoder()
/// encoder.dateEncodingStrategy = .iso8601
/// return encoder
/// }
/// }
static func databaseJSONEncoder(for column: String) -> JSONEncoder
/// When the EncodableRecord type also adopts the standard Encodable
/// protocol, this property controls the encoding of date properties.
///
/// Default value is .deferredToDate
///
/// For example:
///
/// struct Player: PersistableRecord, Encodable {
/// static let databaseDateEncodingStrategy: DatabaseDateEncodingStrategy = .timeIntervalSince1970
///
/// var name: String
/// var registrationDate: Date // encoded as an epoch timestamp
/// }
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
/// When the EncodableRecord type also adopts the standard Encodable
/// protocol, this property controls the encoding of UUID properties.
///
/// Default value is .deferredToUUID
///
/// For example:
///
/// struct Player: PersistableProtocol, Encodable {
/// static let databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy = .string
///
/// // encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
}
extension EncodableRecord {
public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
return [:]
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.nonConformingFloatEncodingStrategy = .throw
if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) {
// guarantee some stability in order to ease record comparison
encoder.outputFormatting = .sortedKeys
}
return encoder
}
public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy {
return .deferredToDate
}
public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy {
return .deferredToUUID
}
}
extension EncodableRecord {
/// A dictionary whose keys are the columns encoded in the `encode(to:)` method.
public var databaseDictionary: [String: DatabaseValue] {
return Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null }
}
}
extension EncodableRecord {
// MARK: - Record Comparison
/// Returns a boolean indicating whether this record and the other record
/// have the same database representation.
public func databaseEquals(_ record: Self) -> Bool {
return PersistenceContainer(self).changesIterator(from: PersistenceContainer(record)).next() == nil
}
/// A dictionary of values changed from the other record.
///
/// Its keys are column names. Its values come from the other record.
///
/// Note that this method is not symmetrical, not only in terms of values,
/// but also in terms of columns. When the two records don't define the
/// same set of columns in their `encode(to:)` method, only the columns
/// defined by the receiver record are considered.
public func databaseChanges<Record: EncodableRecord>(from record: Record) -> [String: DatabaseValue] {
return Dictionary(uniqueKeysWithValues: PersistenceContainer(self).changesIterator(from: PersistenceContainer(record)))
}
}
// MARK: - PersistenceContainer
/// Use persistence containers in the `encode(to:)` method of your
/// encodable records:
///
/// struct Player: EncodableRecord {
/// var id: Int64?
/// var name: String?
///
/// func encode(to container: inout PersistenceContainer) {
/// container["id"] = id
/// container["name"] = name
/// }
/// }
public struct PersistenceContainer {
// fileprivate for Row(_:PersistenceContainer)
// The ordering of the OrderedDictionary helps generating always the same
// SQL queries, and hit the statement cache.
@usableFromInline var storage: OrderedDictionary<String, DatabaseValueConvertible?>
/// Accesses the value associated with the given column.
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
@inlinable
public subscript(_ column: String) -> DatabaseValueConvertible? {
get { return storage[column] ?? nil }
set { storage.updateValue(newValue, forKey: column) }
}
/// Accesses the value associated with the given column.
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
@inlinable
public subscript<Column: ColumnExpression>(_ column: Column) -> DatabaseValueConvertible? {
get { return self[column.name] }
set { self[column.name] = newValue }
}
init() {
storage = OrderedDictionary()
}
init(minimumCapacity: Int) {
storage = OrderedDictionary(minimumCapacity: minimumCapacity)
}
/// Convenience initializer from a record
init<Record: EncodableRecord>(_ record: Record) {
self.init()
record.encode(to: &self)
}
/// Columns stored in the container, ordered like values.
var columns: [String] {
return Array(storage.keys)
}
/// Values stored in the container, ordered like columns.
var values: [DatabaseValueConvertible?] {
return Array(storage.values)
}
/// Accesses the value associated with the given column, in a
/// case-insensitive fashion.
///
/// :nodoc:
subscript(caseInsensitive column: String) -> DatabaseValueConvertible? {
get {
if let value = storage[column] {
return value
}
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value
}
return nil
}
set {
if storage[column] != nil {
storage[column] = newValue
return
}
let lowercaseColumn = column.lowercased()
for key in storage.keys where key.lowercased() == lowercaseColumn {
storage[key] = newValue
return
}
storage[column] = newValue
}
}
// Returns nil if column is not defined
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value?.databaseValue ?? .null
}
return nil
}
var isEmpty: Bool {
return storage.isEmpty
}
/// An iterator over the (column, value) pairs
func makeIterator() -> IndexingIterator<OrderedDictionary<String, DatabaseValueConvertible?>> {
return storage.makeIterator()
}
func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
var newValueIterator = makeIterator()
return AnyIterator {
// Loop until we find a change, or exhaust columns:
while let (column, newValue) = newValueIterator.next() {
let oldValue = container[caseInsensitive: column]
let oldDbValue = oldValue?.databaseValue ?? .null
let newDbValue = newValue?.databaseValue ?? .null
if newDbValue != oldDbValue {
return (column, oldDbValue)
}
}
return nil
}
}
}
extension Row {
convenience init<Record: EncodableRecord>(_ record: Record) {
self.init(PersistenceContainer(record))
}
convenience init(_ container: PersistenceContainer) {
self.init(Dictionary(container.storage))
}
}
// MARK: - DatabaseDateEncodingStrategy
/// DatabaseDateEncodingStrategy specifies how EncodableRecord types that also
/// adopt the standard Encodable protocol encode their date properties.
///
/// For example:
///
/// struct Player: EncodableRecord, Encodable {
/// static let databaseDateEncodingStrategy: DatabaseDateEncodingStrategy = .timeIntervalSince1970
///
/// var name: String
/// var registrationDate: Date // encoded as an epoch timestamp
/// }
public enum DatabaseDateEncodingStrategy {
/// The strategy that uses formatting from the Date structure.
///
/// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the
/// UTC time zone.
case deferredToDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 2001
case timeIntervalSinceReferenceDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case timeIntervalSince1970
/// Encodes an Int64: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case secondsSince1970
/// Encodes an Int64: the number of milliseconds between the date and
/// midnight UTC on 1 January 1970
case millisecondsSince1970
/// Encodes dates according to the ISO 8601 and RFC 3339 standards
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
/// Encodes a String, according to the provided formatter
case formatted(DateFormatter)
/// Encodes the result of the user-provided function
case custom((Date) -> DatabaseValueConvertible?)
}
// MARK: - DatabaseUUIDEncodingStrategy
/// DatabaseUUIDEncodingStrategy specifies how EncodableRecord types that also
/// adopt the standard Encodable protocol encode their UUID properties.
///
/// For example:
///
/// struct Player: EncodableProtocol, Encodable {
/// static let databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy = .string
///
/// // encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
public enum DatabaseUUIDEncodingStrategy {
/// The strategy that uses formatting from the UUID type.
///
/// It encodes UUIDs as 16-bytes data blobs.
case deferredToUUID
/// Encodes UUIDs as strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
case string
}

View File

@ -24,6 +24,7 @@ extension FetchableRecord where Self: TableRecord {
/// - parameter db: A database connection.
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ db: Database) throws -> RecordCursor<Self> {
return try all().fetchCursor(db)
}
@ -39,6 +40,7 @@ extension FetchableRecord where Self: TableRecord {
///
/// - parameter db: A database connection.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ db: Database) throws -> [Self] {
return try all().fetchAll(db)
}
@ -54,6 +56,7 @@ extension FetchableRecord where Self: TableRecord {
///
/// - parameter db: A database connection.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ db: Database) throws -> Self? {
return try all().fetchOne(db)
}
@ -77,6 +80,7 @@ extension FetchableRecord where Self: TableRecord {
/// - keys: A sequence of primary keys.
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> RecordCursor<Self> where Sequence.Element: DatabaseValueConvertible {
return try filter(keys: keys).fetchCursor(db)
}
@ -92,6 +96,7 @@ extension FetchableRecord where Self: TableRecord {
/// - keys: A sequence of primary keys.
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> [Self] where Sequence.Element: DatabaseValueConvertible {
let keys = Array(keys)
if keys.isEmpty {
@ -110,6 +115,7 @@ extension FetchableRecord where Self: TableRecord {
/// - key: A primary key value.
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<PrimaryKeyType: DatabaseValueConvertible>(_ db: Database, key: PrimaryKeyType?) throws -> Self? {
guard let key = key else {
// Avoid hitting the database
@ -138,6 +144,7 @@ extension FetchableRecord where Self: TableRecord {
/// - keys: An array of key dictionaries.
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> RecordCursor<Self> {
return try filter(keys: keys).fetchCursor(db)
}
@ -154,6 +161,7 @@ extension FetchableRecord where Self: TableRecord {
/// - keys: An array of key dictionaries.
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> [Self] {
if keys.isEmpty {
// Avoid hitting the database
@ -172,6 +180,7 @@ extension FetchableRecord where Self: TableRecord {
/// - key: A dictionary of values.
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ db: Database, key: [String: DatabaseValueConvertible?]?) throws -> Self? {
guard let key = key else {
// Avoid hitting the database

View File

@ -7,17 +7,17 @@ import Foundation
/// Types that adopt FetchableRecord can be initialized from a database Row.
///
/// let row = try Row.fetchOne(db, "SELECT ...")!
/// let row = try Row.fetchOne(db, sql: "SELECT ...")!
/// let player = Player(row)
///
/// The protocol comes with built-in methods that allow to fetch cursors,
/// arrays, or single records:
///
/// try Player.fetchCursor(db, "SELECT ...", arguments:...) // Cursor of Player
/// try Player.fetchAll(db, "SELECT ...", arguments:...) // [Player]
/// try Player.fetchOne(db, "SELECT ...", arguments:...) // Player?
/// try Player.fetchCursor(db, sql: "SELECT ...", arguments:...) // Cursor of Player
/// try Player.fetchAll(db, sql: "SELECT ...", arguments:...) // [Player]
/// try Player.fetchOne(db, sql: "SELECT ...", arguments:...) // Player?
///
/// let statement = try db.makeSelectStatement("SELECT ...")
/// let statement = try db.makeSelectStatement(sql: "SELECT ...")
/// try Player.fetchCursor(statement, arguments:...) // Cursor of Player
/// try Player.fetchAll(statement, arguments:...) // [Player]
/// try Player.fetchOne(statement, arguments:...) // Player?
@ -141,7 +141,7 @@ extension FetchableRecord {
/// A cursor over records fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT * FROM player")
/// let statement = try db.makeSelectStatement(sql: "SELECT * FROM player")
/// let players = try Player.fetchCursor(statement) // Cursor of Player
/// while let player = try players.next() { // Player
/// ...
@ -158,13 +158,14 @@ extension FetchableRecord {
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RecordCursor<Self> {
return try RecordCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of records fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT * FROM player")
/// let statement = try db.makeSelectStatement(sql: "SELECT * FROM player")
/// let players = try Player.fetchAll(statement) // [Player]
///
/// - parameters:
@ -173,13 +174,14 @@ extension FetchableRecord {
/// - adapter: Optional RowAdapter
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
/// Returns a single record fetched from a prepared statement.
///
/// let statement = try db.makeSelectStatement("SELECT * FROM player")
/// let statement = try db.makeSelectStatement(sql: "SELECT * FROM player")
/// let player = try Player.fetchOne(statement) // Player?
///
/// - parameters:
@ -188,6 +190,7 @@ extension FetchableRecord {
/// - adapter: Optional RowAdapter
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
return try fetchCursor(statement, arguments: arguments, adapter: adapter).next()
}
@ -199,7 +202,7 @@ extension FetchableRecord {
/// Returns a cursor over records fetched from an SQL query.
///
/// let players = try Player.fetchCursor(db, "SELECT * FROM player") // Cursor of Player
/// let players = try Player.fetchCursor(db, sql: "SELECT * FROM player") // Cursor of Player
/// while let player = try players.next() { // Player
/// ...
/// }
@ -212,42 +215,45 @@ extension FetchableRecord {
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RecordCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchCursor(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> RecordCursor<Self> {
return try fetchCursor(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of records fetched from an SQL query.
///
/// let players = try Player.fetchAll(db, "SELECT * FROM player") // [Player]
/// let players = try Player.fetchAll(db, sql: "SELECT * FROM player") // [Player]
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchAll(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> [Self] {
return try fetchAll(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single record fetched from an SQL query.
///
/// let player = try Player.fetchOne(db, "SELECT * FROM player") // Player?
/// let player = try Player.fetchOne(db, sql: "SELECT * FROM player") // Player?
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL query.
/// - arguments: Optional statement arguments.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql, arguments: arguments, adapter: adapter))
@inlinable
public static func fetchOne(_ db: Database, sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws -> Self? {
return try fetchOne(db, SQLRequest<Void>(sql: sql, arguments: arguments, adapter: adapter))
}
}
@ -273,6 +279,7 @@ extension FetchableRecord {
/// - sql: a FetchRequest.
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> RecordCursor<Self> {
let (statement, adapter) = try request.prepare(db)
return try fetchCursor(statement, adapter: adapter)
@ -288,6 +295,7 @@ extension FetchableRecord {
/// - sql: a FetchRequest.
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Self] {
let (statement, adapter) = try request.prepare(db)
return try fetchAll(statement, adapter: adapter)
@ -303,6 +311,7 @@ extension FetchableRecord {
/// - sql: a FetchRequest.
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
return try fetchOne(statement, adapter: adapter)
@ -331,6 +340,7 @@ extension FetchRequest where RowDecoder: FetchableRecord {
/// - parameter db: A database connection.
/// - returns: A cursor over fetched records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchCursor(_ db: Database) throws -> RecordCursor<RowDecoder> {
return try RowDecoder.fetchCursor(db, self)
}
@ -343,6 +353,7 @@ extension FetchRequest where RowDecoder: FetchableRecord {
/// - parameter db: A database connection.
/// - returns: An array of records.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
return try RowDecoder.fetchAll(db, self)
}
@ -355,6 +366,7 @@ extension FetchRequest where RowDecoder: FetchableRecord {
/// - parameter db: A database connection.
/// - returns: An optional record.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchOne(_ db: Database) throws -> RowDecoder? {
return try RowDecoder.fetchOne(db, self)
}
@ -366,43 +378,44 @@ extension FetchRequest where RowDecoder: FetchableRecord {
///
/// struct Player : FetchableRecord { ... }
/// try dbQueue.read { db in
/// let players: RecordCursor<Player> = try Player.fetchCursor(db, "SELECT * FROM player")
/// let players: RecordCursor<Player> = try Player.fetchCursor(db, sql: "SELECT * FROM player")
/// }
public final class RecordCursor<Record: FetchableRecord> : Cursor {
private let statement: SelectStatement
private let row: Row // Reused for performance
private let sqliteStatement: SQLiteStatement
private var done = false
@usableFromInline let _statement: SelectStatement
@usableFromInline let _row: Row // Reused for performance
@usableFromInline let _sqliteStatement: SQLiteStatement
@usableFromInline var _done = false
@inlinable
init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
self.statement = statement
self.row = try Row(statement: statement).adapted(with: adapter, layout: statement)
self.sqliteStatement = statement.sqliteStatement
statement.reset(withArguments: arguments)
_statement = statement
_row = try Row(statement: statement).adapted(with: adapter, layout: statement)
_sqliteStatement = statement.sqliteStatement
_statement.reset(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? statement.reset()
try? _statement.reset()
}
/// :nodoc:
@inlinable
public func next() throws -> Record? {
if done {
if _done {
// make sure this instance never yields a value again, even if the
// statement is reset by another cursor.
return nil
}
switch sqlite3_step(sqliteStatement) {
switch sqlite3_step(_sqliteStatement) {
case SQLITE_DONE:
done = true
_done = true
return nil
case SQLITE_ROW:
return Record(row: row)
return Record(row: _row)
case let code:
statement.database.selectStatementDidFail(statement)
throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments)
try _statement.didFail(withResultCode: code)
}
}
}

View File

@ -40,14 +40,14 @@ public final class FetchedRecordsController<Record: FetchableRecord> {
public convenience init(
_ databaseWriter: DatabaseWriter,
sql: String,
arguments: StatementArguments? = nil,
arguments: StatementArguments = StatementArguments(),
adapter: RowAdapter? = nil,
queue: DispatchQueue = .main,
isSameRecord: ((Record, Record) -> Bool)? = nil) throws
{
try self.init(
databaseWriter,
request: SQLRequest<Record>(sql, arguments: arguments, adapter: adapter),
request: SQLRequest<Record>(sql: sql, arguments: arguments, adapter: adapter),
queue: queue,
isSameRecord: isSameRecord)
}
@ -196,8 +196,8 @@ public final class FetchedRecordsController<Record: FetchableRecord> {
///
/// This method must be used from the controller's dispatch queue (the
/// main queue unless stated otherwise in the controller's initializer).
public func setRequest(sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws {
try setRequest(SQLRequest(sql, arguments: arguments, adapter: adapter))
public func setRequest(sql: String, arguments: StatementArguments = StatementArguments(), adapter: RowAdapter? = nil) throws {
try setRequest(SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
/// Registers changes notification callbacks.
@ -429,13 +429,13 @@ extension FetchedRecordsController where Record: TableRecord {
public convenience init(
_ databaseWriter: DatabaseWriter,
sql: String,
arguments: StatementArguments? = nil,
arguments: StatementArguments = StatementArguments(),
adapter: RowAdapter? = nil,
queue: DispatchQueue = .main) throws
{
try self.init(
databaseWriter,
request: SQLRequest(sql, arguments: arguments, adapter: adapter),
request: SQLRequest(sql: sql, arguments: arguments, adapter: adapter),
queue: queue)
}
@ -879,7 +879,7 @@ extension FetchedRecordsController {
}
}
extension FetchedRecordsController where Record: MutablePersistableRecord {
extension FetchedRecordsController where Record: EncodableRecord {
/// Returns the indexPath of a given record.
///
@ -887,7 +887,7 @@ extension FetchedRecordsController where Record: MutablePersistableRecord {
/// if record could not be found.
public func indexPath(for record: Record) -> IndexPath? {
let item = Item<Record>(row: Row(record))
guard let fetchedItems = fetchedItems, let index = fetchedItems.index(where: { itemsAreIdentical($0, item) }) else {
guard let fetchedItems = fetchedItems, let index = fetchedItems.firstIndex(where: { itemsAreIdentical($0, item) }) else {
return nil
}
return IndexPath(indexes: [0, index])

View File

@ -1,6 +1,5 @@
import Foundation
extension Database.ConflictResolution {
@usableFromInline
var invalidatesLastInsertedRowID: Bool {
switch self {
case .abort, .fail, .rollback, .replace:
@ -12,14 +11,15 @@ extension Database.ConflictResolution {
}
}
// MARK: - PersistenceError
/// An error thrown by a type that adopts PersistableRecord.
public enum PersistenceError: Error, CustomStringConvertible {
/// Thrown by MutablePersistableRecord.update() when no matching row could be
/// found in the database.
case recordNotFound(MutablePersistableRecord)
///
/// - databaseTableName: the table of the unfound record
/// - key: the key of the unfound record (column and values)
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
}
// CustomStringConvertible
@ -27,138 +27,13 @@ extension PersistenceError {
/// :nodoc:
public var description: String {
switch self {
case .recordNotFound(let record):
return "Not found: \(record)"
case let .recordNotFound(databaseTableName: databaseTableName, key: key):
let row = Row(key) // For nice output
return "Key not found in table \(databaseTableName): \(row.description)"
}
}
}
// MARK: - PersistenceContainer
/// Use persistence containers in the `encode(to:)` method of your
/// encodable records:
///
/// struct Player : MutablePersistableRecord {
/// var id: Int64?
/// var name: String?
///
/// func encode(to container: inout PersistenceContainer) {
/// container["id"] = id
/// container["name"] = name
/// }
/// }
public struct PersistenceContainer {
// fileprivate for Row(_:PersistenceContainer)
fileprivate var storage: [String: DatabaseValueConvertible?]
/// Accesses the value associated with the given column.
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
public subscript(_ column: String) -> DatabaseValueConvertible? {
get { return storage[column] ?? nil }
set { storage.updateValue(newValue, forKey: column) }
}
/// Accesses the value associated with the given column.
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
public subscript(_ column: ColumnExpression) -> DatabaseValueConvertible? {
get { return self[column.name] }
set { self[column.name] = newValue }
}
init() {
storage = [:]
}
/// Convenience initializer from a record
///
/// // Sweet
/// let container = PersistenceContainer(record)
///
/// // Meh
/// var container = PersistenceContainer()
/// record.encode(to: container)
init(_ record: MutablePersistableRecord) {
storage = [:]
record.encode(to: &self)
}
/// Columns stored in the container, ordered like values.
var columns: [String] {
return Array(storage.keys)
}
/// Values stored in the container, ordered like columns.
var values: [DatabaseValueConvertible?] {
return Array(storage.values)
}
/// Accesses the value associated with the given column, in a
/// case-insensitive fashion.
///
/// :nodoc:
subscript(caseInsensitive column: String) -> DatabaseValueConvertible? {
get {
if let value = storage[column] {
return value
}
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value
}
return nil
}
set {
if storage[column] != nil {
storage[column] = newValue
return
}
let lowercaseColumn = column.lowercased()
for key in storage.keys where key.lowercased() == lowercaseColumn {
storage[key] = newValue
return
}
storage[column] = newValue
}
}
// Returns nil if column is not defined
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value?.databaseValue ?? .null
}
return nil
}
var isEmpty: Bool {
return storage.isEmpty
}
/// An iterator over the (column, value) pairs
func makeIterator() -> DictionaryIterator<String, DatabaseValueConvertible?> {
return storage.makeIterator()
}
}
extension Row {
convenience init(_ record: MutablePersistableRecord) {
self.init(PersistenceContainer(record))
}
convenience init(_ container: PersistenceContainer) {
self.init(container.storage)
}
}
// MARK: - MutablePersistableRecord
/// The MutablePersistableRecord protocol uses this type in order to handle SQLite
/// conflicts when records are inserted or updated.
///
@ -180,7 +55,7 @@ public struct PersistenceConflictPolicy {
}
/// Types that adopt MutablePersistableRecord can be inserted, updated, and deleted.
public protocol MutablePersistableRecord : TableRecord {
public protocol MutablePersistableRecord : EncodableRecord, TableRecord {
/// The policy that handles SQLite conflicts when records are inserted
/// or updated.
///
@ -195,28 +70,6 @@ public protocol MutablePersistableRecord : TableRecord {
/// See https://www.sqlite.org/lang_conflict.html
static var persistenceConflictPolicy: PersistenceConflictPolicy { get }
/// Defines the values persisted in the database.
///
/// Store in the *container* argument all values that should be stored in
/// the columns of the database table (see databaseTableName()).
///
/// Primary key columns, if any, must be included.
///
/// struct Player : MutablePersistableRecord {
/// var id: Int64?
/// var name: String?
///
/// func encode(to container: inout PersistenceContainer) {
/// container["id"] = id
/// container["name"] = name
/// }
/// }
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
func encode(to container: inout PersistenceContainer)
/// Notifies the record that it was succesfully inserted.
///
/// Do not call this method directly: it is called for you, in a protected
@ -322,134 +175,6 @@ public protocol MutablePersistableRecord : TableRecord {
/// - returns: Whether the primary key matches a row in the database.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
func exists(_ db: Database) throws -> Bool
// MARK: - Customizing the Format of Database Columns
/// When the PersistableRecord type also adopts the standard Encodable
/// protocol, you can use this dictionary to customize the encoding process
/// into database rows.
///
/// For example:
///
/// // A key that holds a encoder's name
/// let encoderName = CodingUserInfoKey(rawValue: "encoderName")!
///
/// // A PersistableRecord + Encodable record
/// struct Player: PersistableRecord, Encodable {
/// // Customize the encoder name when encoding a database row
/// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"]
///
/// func encode(to encoder: Encoder) throws {
/// // Print the encoder name
/// print(encoder.userInfo[encoderName])
/// ...
/// }
/// }
///
/// let player = Player(...)
///
/// // prints "Database"
/// try player.insert(db)
///
/// // prints "JSON"
/// let encoder = JSONEncoder()
/// encoder.userInfo = [encoderName: "JSON"]
/// let data = try encoder.encode(player)
static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
/// When the PersistableRecord type also adopts the standard Encodable
/// protocol, this method controls the encoding process of nested properties
/// into JSON database columns.
///
/// The default implementation returns a JSONEncoder with the
/// following properties:
///
/// - dataEncodingStrategy: .base64
/// - dateEncodingStrategy: .millisecondsSince1970
/// - nonConformingFloatEncodingStrategy: .throw
/// - outputFormatting: .sortedKeys (iOS 11.0+, macOS 10.13+, watchOS 4.0+)
///
/// You can override those defaults:
///
/// struct Achievement: Encodable {
/// var name: String
/// var date: Date
/// }
///
/// struct Player: Encodable, PersistableRecord {
/// // stored in a JSON column
/// var achievements: [Achievement]
///
/// static func databaseJSONEncoder(for column: String) -> JSONEncoder {
/// let encoder = JSONEncoder()
/// encoder.dateEncodingStrategy = .iso8601
/// return encoder
/// }
/// }
static func databaseJSONEncoder(for column: String) -> JSONEncoder
/// When the PersistableRecord type also adopts the standard Encodable
/// protocol, this property controls the encoding of date properties.
///
/// Default value is .deferredToDate
///
/// For example:
///
/// struct Player: PersistableRecord, Encodable {
/// static let databaseDateEncodingStrategy: DatabaseDateEncodingStrategy = .timeIntervalSince1970
///
/// var name: String
/// var registrationDate: Date // encoded as an epoch timestamp
/// }
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
/// When the PersistableRecord type also adopts the standard Encodable
/// protocol, this property controls the encoding of UUID properties.
///
/// Default value is .deferredToUUID
///
/// For example:
///
/// struct Player: PersistableProtocol, Encodable {
/// static let databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy = .string
///
/// // encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
}
extension MutablePersistableRecord {
public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
return [:]
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.nonConformingFloatEncodingStrategy = .throw
if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) {
// guarantee some stability in order to ease record comparison
encoder.outputFormatting = .sortedKeys
}
return encoder
}
public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy {
return .deferredToDate
}
public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy {
return .deferredToUUID
}
}
extension MutablePersistableRecord {
/// A dictionary whose keys are the columns encoded in the `encode(to:)` method.
public var databaseDictionary: [String: DatabaseValue] {
return PersistenceContainer(self).storage.mapValues { $0?.databaseValue ?? .null }
}
}
extension MutablePersistableRecord {
@ -545,8 +270,8 @@ extension MutablePersistableRecord {
/// match any row in the database and record could not be updated.
/// - SeeAlso: updateChanges(_:with:)
@discardableResult
public func updateChanges(_ db: Database, from record: MutablePersistableRecord) throws -> Bool {
return try updateChanges(db, from: PersistenceContainer(record))
public func updateChanges<Record: MutablePersistableRecord>(_ db: Database, from record: Record) throws -> Bool {
return try updateChanges(db, from: PersistenceContainer(db, record))
}
/// Mutates the record according to the provided closure, and then, if the
@ -574,7 +299,7 @@ extension MutablePersistableRecord {
/// match any row in the database and record could not be updated.
@discardableResult
public mutating func updateChanges(_ db: Database, with change: (inout Self) throws -> Void) throws -> Bool {
let container = PersistenceContainer(self)
let container = try PersistenceContainer(db, self)
try change(&self)
return try updateChanges(db, from: container)
}
@ -605,43 +330,9 @@ extension MutablePersistableRecord {
// MARK: - Record Comparison
/// Returns a boolean indicating whether this record and the other record
/// have the same database representation.
public func databaseEquals(_ record: Self) -> Bool {
return databaseChangesIterator(from: PersistenceContainer(record)).next() == nil
}
/// A dictionary of values changed from the other record.
///
/// Its keys are column names. Its values come from the other record.
///
/// Note that this method is not symmetrical, not only in terms of values,
/// but also in terms of columns. When the two records don't define the
/// same set of columns in their `encode(to:)` method, only the columns
/// defined by the receiver record are considered.
public func databaseChanges(from record: MutablePersistableRecord) -> [String: DatabaseValue] {
return Dictionary(uniqueKeysWithValues: databaseChangesIterator(from: PersistenceContainer(record)))
}
private func databaseChangesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
var newValueIterator = PersistenceContainer(self).makeIterator()
return AnyIterator {
// Loop until we find a change, or exhaust columns:
while let (column, newValue) = newValueIterator.next() {
let oldValue = container[caseInsensitive: column]
let oldDbValue = oldValue?.databaseValue ?? .null
let newDbValue = newValue?.databaseValue ?? .null
if newDbValue != oldDbValue {
return (column, oldDbValue)
}
}
return nil
}
}
@discardableResult
fileprivate func updateChanges(_ db: Database, from container: PersistenceContainer) throws -> Bool {
let changes = databaseChangesIterator(from: container)
let changes = try PersistenceContainer(db, self).changesIterator(from: container)
let changedColumns: Set<String> = changes.reduce(into: []) { $0.insert($1.0) }
if changedColumns.isEmpty {
return false
@ -652,17 +343,19 @@ extension MutablePersistableRecord {
// MARK: - CRUD Internals
/// Return true if record has a non-null primary key
fileprivate func canUpdate(_ db: Database) throws -> Bool {
/// Return a non-nil dictionary if record has a non-null primary key
@usableFromInline
func primaryKey(_ db: Database) throws -> [String: DatabaseValue]? {
let databaseTableName = type(of: self).databaseTableName
let primaryKey = try db.primaryKey(databaseTableName)
let container = PersistenceContainer(self)
for column in primaryKey.columns {
if let value = container[caseInsensitive: column], !value.databaseValue.isNull {
return true
}
let primaryKeyInfo = try db.primaryKey(databaseTableName)
let container = try PersistenceContainer(db, self)
let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map {
($0, container[caseInsensitive: $0]?.databaseValue ?? .null)
})
if primaryKey.allSatisfy({ $0.value.isNull }) {
return nil
}
return false
return primaryKey
}
/// Don't invoke this method directly: it is an internal method for types
@ -672,6 +365,7 @@ extension MutablePersistableRecord {
/// that adopt MutablePersistableRecord can invoke performInsert() in their
/// implementation of insert(). They should not provide their own
/// implementation of performInsert().
@inlinable
public mutating func performInsert(_ db: Database) throws {
let conflictResolutionForInsert = type(of: self).persistenceConflictPolicy.conflictResolutionForInsert
let dao = try DAO(db, self)
@ -695,14 +389,16 @@ extension MutablePersistableRecord {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
/// PersistenceError.recordNotFound is thrown if the primary key does not
/// match any row in the database.
@inlinable
public func performUpdate(_ db: Database, columns: Set<String>) throws {
guard let statement = try DAO(db, self).updateStatement(db, columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
let dao = try DAO(db, self)
guard let statement = try dao.updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
// Nil primary key
throw PersistenceError.recordNotFound(self)
throw dao.makeRecordNotFoundError()
}
try statement.execute()
if db.changesCount == 0 {
throw PersistenceError.recordNotFound(self)
throw dao.makeRecordNotFoundError()
}
}
@ -715,19 +411,14 @@ extension MutablePersistableRecord {
/// implementation of performSave().
///
/// This default implementation forwards the job to `update` or `insert`.
@inlinable
public mutating func performSave(_ db: Database) throws {
// Make sure we call self.insert and self.update so that classes
// that override insert or save have opportunity to perform their
// custom job.
if try canUpdate(db) {
// Call self.insert and self.update so that we support classes that
// override those methods.
if let key = try primaryKey(db) {
do {
try update(db)
} catch PersistenceError.recordNotFound {
// TODO: check that the not persisted objet is self
//
// Why? Adopting types could override update() and update
// another object which may be the one throwing this error.
} catch PersistenceError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
try insert(db)
}
} else {
@ -742,6 +433,7 @@ extension MutablePersistableRecord {
/// that adopt MutablePersistableRecord can invoke performDelete() in
/// their implementation of delete(). They should not provide their own
/// implementation of performDelete().
@inlinable
public func performDelete(_ db: Database) throws -> Bool {
guard let statement = try DAO(db, self).deleteStatement() else {
// Nil primary key
@ -758,6 +450,7 @@ extension MutablePersistableRecord {
/// that adopt MutablePersistableRecord can invoke performExists() in
/// their implementation of exists(). They should not provide their own
/// implementation of performExists().
@inlinable
public func performExists(_ db: Database) throws -> Bool {
guard let statement = try DAO(db, self).existsStatement() else {
// Nil primary key
@ -796,11 +489,10 @@ extension MutablePersistableRecord where Self: AnyObject {
/// match any row in the database and record could not be updated.
@discardableResult
public func updateChanges(_ db: Database, with change: (Self) throws -> Void) throws -> Bool {
let container = PersistenceContainer(self)
let container = try PersistenceContainer(db, self)
try change(self)
return try updateChanges(db, from: container)
}
}
extension MutablePersistableRecord {
@ -1017,6 +709,7 @@ extension PersistableRecord {
/// that adopt PersistableRecord can invoke performInsert() in their
/// implementation of insert(). They should not provide their own
/// implementation of performInsert().
@inlinable
public func performInsert(_ db: Database) throws {
let conflictResolutionForInsert = type(of: self).persistenceConflictPolicy.conflictResolutionForInsert
let dao = try DAO(db, self)
@ -1036,18 +729,14 @@ extension PersistableRecord {
/// implementation of performSave().
///
/// This default implementation forwards the job to `update` or `insert`.
@inlinable
public func performSave(_ db: Database) throws {
// Make sure we call self.insert and self.update so that classes that
// override insert or save have opportunity to perform their custom job.
if try canUpdate(db) {
// Call self.insert and self.update so that we support classes that
// override those methods.
if let key = try primaryKey(db) {
do {
try update(db)
} catch PersistenceError.recordNotFound {
// TODO: check that the not persisted objet is self
//
// Why? Adopting types could override update() and update another
// object which may be the one throwing this error.
} catch PersistenceError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
try insert(db)
}
} else {
@ -1056,88 +745,24 @@ extension PersistableRecord {
}
}
// MARK: - DatabaseDateEncodingStrategy
/// DatabaseDateEncodingStrategy specifies how PersistableRecord types that also
/// adopt the standard Encodable protocol encode their date properties.
///
/// For example:
///
/// struct Player: PersistableRecord, Encodable {
/// static let databaseDateEncodingStrategy: DatabaseDateEncodingStrategy = .timeIntervalSince1970
///
/// var name: String
/// var registrationDate: Date // encoded as an epoch timestamp
/// }
public enum DatabaseDateEncodingStrategy {
/// The strategy that uses formatting from the Date structure.
///
/// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the
/// UTC time zone.
case deferredToDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 2001
case timeIntervalSinceReferenceDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case timeIntervalSince1970
/// Encodes an Int64: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case secondsSince1970
/// Encodes an Int64: the number of milliseconds between the date and
/// midnight UTC on 1 January 1970
case millisecondsSince1970
/// Encodes dates according to the ISO 8601 and RFC 3339 standards
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
/// Encodes a String, according to the provided formatter
case formatted(DateFormatter)
/// Encodes the result of the user-provided function
case custom((Date) -> DatabaseValueConvertible?)
}
// MARK: - DatabaseUUIDEncodingStrategy
/// DatabaseUUIDEncodingStrategy specifies how FetchableRecord types that also
/// adopt the standard Encodable protocol encode their UUID properties.
///
/// For example:
///
/// struct Player: PersistableProtocol, Encodable {
/// static let databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy = .string
///
/// // encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
public enum DatabaseUUIDEncodingStrategy {
/// The strategy that uses formatting from the UUID type.
///
/// It encodes UUIDs as 16-bytes data blobs.
case deferredToUUID
/// Encodes UUIDs as strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
case string
}
// MARK: - DAO
extension PersistenceContainer {
/// Convenience initializer from a database connection and a record
init<Record: EncodableRecord & TableRecord>(_ db: Database,_ record: Record) throws {
let databaseTableName = type(of: record).databaseTableName
let columnCount = try db.columns(in: databaseTableName).count
self.init(minimumCapacity: columnCount)
record.encode(to: &self)
}
}
/// DAO takes care of PersistableRecord CRUD
final class DAO {
@usableFromInline
final class DAO<Record: MutablePersistableRecord> {
/// The database
let db: Database
/// The record
let record: MutablePersistableRecord
/// DAO keeps a copy the record's persistenceContainer, so that this
/// dictionary is built once whatever the database operation. It is
/// guaranteed to have at least one (key, value) pair.
@ -1146,41 +771,40 @@ final class DAO {
/// The table name
let databaseTableName: String
/// The table primary key
let primaryKey: PrimaryKeyInfo
/// The table primary key info
@usableFromInline let primaryKey: PrimaryKeyInfo
init(_ db: Database, _ record: MutablePersistableRecord) throws {
let databaseTableName = type(of: record).databaseTableName
let primaryKey = try db.primaryKey(databaseTableName)
let persistenceContainer = PersistenceContainer(record)
GRDBPrecondition(!persistenceContainer.isEmpty, "\(type(of: record)): invalid empty persistence container")
@usableFromInline
init(_ db: Database, _ record: Record) throws {
self.db = db
self.record = record
self.persistenceContainer = persistenceContainer
self.databaseTableName = databaseTableName
self.primaryKey = primaryKey
databaseTableName = type(of: record).databaseTableName
primaryKey = try db.primaryKey(databaseTableName)
persistenceContainer = try PersistenceContainer(db, record)
GRDBPrecondition(!persistenceContainer.isEmpty, "\(type(of: record)): invalid empty persistence container")
}
@usableFromInline
func insertStatement(onConflict: Database.ConflictResolution) throws -> UpdateStatement {
let query = InsertQuery(
onConflict: onConflict,
tableName: databaseTableName,
insertedColumns: persistenceContainer.columns)
let statement = try db.internalCachedUpdateStatement(query.sql)
let statement = try db.internalCachedUpdateStatement(sql: query.sql)
statement.unsafeSetArguments(StatementArguments(persistenceContainer.values))
return statement
}
/// Returns nil if and only if primary key is nil
func updateStatement(_ db: Database, columns: Set<String>, onConflict: Database.ConflictResolution) throws -> UpdateStatement? {
@usableFromInline
func updateStatement(columns: Set<String>, onConflict: Database.ConflictResolution) throws -> UpdateStatement? {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
}
guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil }
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
}
// Don't update columns not present in columns
// Don't update columns not present in the persistenceContainer
@ -1215,44 +839,61 @@ final class DAO {
tableName: databaseTableName,
updatedColumns: updatedColumns,
conditionColumns: primaryKeyColumns)
let statement = try db.internalCachedUpdateStatement(query.sql)
let statement = try db.internalCachedUpdateStatement(sql: query.sql)
statement.unsafeSetArguments(StatementArguments(updatedValues + primaryKeyValues))
return statement
}
/// Returns nil if and only if primary key is nil
@usableFromInline
func deleteStatement() throws -> UpdateStatement? {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
}
guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil }
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
}
let query = DeleteQuery(
tableName: databaseTableName,
conditionColumns: primaryKeyColumns)
let statement = try db.internalCachedUpdateStatement(query.sql)
let statement = try db.internalCachedUpdateStatement(sql: query.sql)
statement.unsafeSetArguments(StatementArguments(primaryKeyValues))
return statement
}
/// Returns nil if and only if primary key is nil
@usableFromInline
func existsStatement() throws -> SelectStatement? {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
}
guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil }
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
}
let query = ExistsQuery(
tableName: databaseTableName,
conditionColumns: primaryKeyColumns)
let statement = try db.internalCachedSelectStatement(query.sql)
let statement = try db.internalCachedSelectStatement(sql: query.sql)
statement.unsafeSetArguments(StatementArguments(primaryKeyValues))
return statement
}
/// Throws a PersistenceError.recordNotFound error
@usableFromInline
func makeRecordNotFoundError() -> Error {
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null)
})
return PersistenceError.recordNotFound(
databaseTableName: databaseTableName,
key: key)
}
}
@ -1262,10 +903,6 @@ private struct InsertQuery: Hashable {
let onConflict: Database.ConflictResolution
let tableName: String
let insertedColumns: [String]
#if !swift(>=4.2)
var hashValue: Int { return tableName.hashValue }
#endif
}
extension InsertQuery {
@ -1296,10 +933,6 @@ private struct UpdateQuery: Hashable {
let tableName: String
let updatedColumns: [String]
let conditionColumns: [String]
#if !swift(>=4.2)
var hashValue: Int { return tableName.hashValue }
#endif
}
extension UpdateQuery {

View File

@ -274,13 +274,13 @@ open class Record : FetchableRecord, TableRecord, PersistableRecord {
//
// But this would trigger two calls to `encode(to:)`.
let dao = try DAO(db, self)
guard let statement = try dao.updateStatement(db, columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
guard let statement = try dao.updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
// Nil primary key
throw PersistenceError.recordNotFound(self)
throw dao.makeRecordNotFoundError()
}
try statement.execute()
if db.changesCount == 0 {
throw PersistenceError.recordNotFound(self)
throw dao.makeRecordNotFoundError()
}
// Set hasDatabaseChanges to false

View File

@ -122,7 +122,7 @@ extension TableRecord {
public static func selectionSQL(alias: String? = nil) -> String {
let alias = TableAlias(tableName: databaseTableName, userName: alias)
let selection = databaseSelection.map { $0.qualifiedSelectable(with: alias) }
var context = SQLGenerationContext.recordSelectionGenerationContext(alias: alias)
var context = SQLGenerationContext.recordSelectionGenerationContext()
return selection
.map { $0.resultColumnSQL(&context) }
.joined(separator: ", ")

View File

@ -8,9 +8,10 @@
/// dict["bar"] // 2
/// dict["qux"] // nil
/// dict.map { $0.key } // ["foo", "bar"], in this order.
@usableFromInline
struct OrderedDictionary<Key: Hashable, Value> {
private(set) var keys: [Key]
private(set) var dictionary: [Key: Value]
@usableFromInline /* private(set) */ var keys: [Key]
@usableFromInline /* private(set) */ var dictionary: [Key: Value]
var values: [Value] {
return keys.map { dictionary[$0]! }
@ -28,8 +29,9 @@ struct OrderedDictionary<Key: Hashable, Value> {
keys.reserveCapacity(minimumCapacity)
dictionary = Dictionary(minimumCapacity: minimumCapacity)
}
/// Returns the value associated with key, or nil.
@inlinable
subscript(_ key: Key) -> Value? {
get { return dictionary[key] }
set {
@ -59,6 +61,7 @@ struct OrderedDictionary<Key: Hashable, Value> {
/// original value. If the given key is not present in the dictionary, this
/// method appends the key-value pair and returns nil.
@discardableResult
@inlinable
mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
if let oldValue = dictionary.updateValue(value, forKey: key) {
return oldValue
@ -69,11 +72,12 @@ struct OrderedDictionary<Key: Hashable, Value> {
/// Removes the value associated with key.
@discardableResult
@usableFromInline
mutating func removeValue(forKey key: Key) -> Value? {
guard let value = dictionary.removeValue(forKey: key) else {
return nil
}
let index = keys.index { $0 == key }!
let index = keys.firstIndex { $0 == key }!
keys.remove(at: index)
return value
}
@ -88,28 +92,28 @@ struct OrderedDictionary<Key: Hashable, Value> {
}
extension OrderedDictionary: Collection {
typealias Index = Int
@usableFromInline typealias Index = Int
var startIndex: Int {
@usableFromInline var startIndex: Int {
return 0
}
var endIndex: Int {
@usableFromInline var endIndex: Int {
return keys.count
}
func index(after i: Int) -> Int {
@usableFromInline func index(after i: Int) -> Int {
return i + 1
}
subscript(position: Int) -> (key: Key, value: Value) {
@usableFromInline subscript(position: Int) -> (key: Key, value: Value) {
let key = keys[position]
return (key: key, value: dictionary[key]!)
}
}
extension OrderedDictionary: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Key, Value)...) {
@usableFromInline init(dictionaryLiteral elements: (Key, Value)...) {
self.keys = elements.map { $0.0 }
self.dictionary = Dictionary(uniqueKeysWithValues: elements)
}

View File

@ -1,49 +1,34 @@
enum Result<Value> {
case success(Value)
#if compiler(>=5.0)
typealias Result<Success> = Swift.Result<Success, Error>
#else
enum Result<Success> {
case success(Success)
case failure(Error)
init(value: () throws -> Value) {
init(catching body: () throws -> Success) {
do {
self = try .success(value())
self = try .success(body())
} catch {
self = .failure(error)
}
}
/// Evaluates the given closure when this `Result` is a success, passing the
/// unwrapped value as a parameter.
///
/// Use the `map` method with a closure that does not throw. For example:
///
/// let possibleData: Result<Data> = .success(Data())
/// let possibleInt = possibleData.map { $0.count }
/// try print(possibleInt.unwrap())
/// // Prints "0"
///
/// let noData: Result<Data> = .failure(error)
/// let noInt = noData.map { $0.count }
/// try print(noInt.unwrap())
/// // Throws error
///
/// - parameter transform: A closure that takes the success value of
/// the instance.
/// - returns: A `Result` containing the result of the given closure. If
/// this instance is a failure, returns the same failure.
func map<T>(_ transform: (Value) -> T) -> Result<T> {
func map<T>(_ transform: (Success) -> T) -> Result<T> {
switch self {
case .success(let value):
return .success(transform(value))
case .success(let success):
return .success(transform(success))
case .failure(let error):
return .failure(error)
}
}
func unwrap() throws -> Value {
func get() throws -> Success {
switch self {
case .success(let value):
return value
case .success(let success):
return success
case .failure(let error):
throw error
}
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More