Compare commits
239 Commits
developmen
...
GRDB-4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfa5fbe8ce | ||
|
|
900134729a | ||
|
|
d888423676 | ||
|
|
eac38be34b | ||
|
|
f42a184401 | ||
|
|
5fbf56c373 | ||
|
|
784ebcc8e9 | ||
|
|
df84654554 | ||
|
|
53e513f1f8 | ||
|
|
0074adc023 | ||
|
|
ca8987ad98 | ||
|
|
6460f639ee | ||
|
|
41d93f546c | ||
|
|
80a642260f | ||
|
|
39800b1257 | ||
|
|
d04fc07337 | ||
|
|
11fd8d6b87 | ||
|
|
7183457217 | ||
|
|
cf4e512894 | ||
|
|
cf837855f2 | ||
|
|
916d3931aa | ||
|
|
9d9bbc5f5d | ||
|
|
153233ace4 | ||
|
|
4bae8e9ac5 | ||
|
|
1e617381a8 | ||
|
|
6d5215c8cb | ||
|
|
4ce6271f6d | ||
|
|
10c75d1e5e | ||
|
|
05258b1098 | ||
|
|
1a513e8e6e | ||
|
|
4ae1fb11cc | ||
|
|
9500a74744 | ||
|
|
32f6a45e8f | ||
|
|
a355b9eb03 | ||
|
|
a3c2cb7f1c | ||
|
|
4ba16067d0 | ||
|
|
403c4a894f | ||
|
|
7ff21981f0 | ||
|
|
5d9fa76754 | ||
|
|
bd633e25ee | ||
|
|
87cd76a468 | ||
|
|
fca6420053 | ||
|
|
ec51f4dbdf | ||
|
|
916311a851 | ||
|
|
d6b6028e42 | ||
|
|
c756d27808 | ||
|
|
13985d0553 | ||
|
|
0363ca406d | ||
|
|
c20b7e5799 | ||
|
|
7642eb054b | ||
|
|
f3c68420a0 | ||
|
|
223ed4c73c | ||
|
|
99d010cded | ||
|
|
5bf1e6f818 | ||
|
|
06f0961b13 | ||
|
|
addfa45d3a | ||
|
|
6489c80846 | ||
|
|
9e8e00ff99 | ||
|
|
9ee25fdec1 | ||
|
|
4d5e5e1de3 | ||
|
|
1188969144 | ||
|
|
66c72e7bd2 | ||
|
|
eed30c0954 | ||
|
|
8dd9f5fd40 | ||
|
|
7cab5dbf2b | ||
|
|
6fd14d0a70 | ||
|
|
51c0b7a2f8 | ||
|
|
679f989e96 | ||
|
|
1a24f61723 | ||
|
|
1ff5552d5e | ||
|
|
4815807f09 | ||
|
|
8fc23e1771 | ||
|
|
5cf2d4920d | ||
|
|
3d6d1c89b2 | ||
|
|
08451dfced | ||
|
|
b506838519 | ||
|
|
1511981dc7 | ||
|
|
661cca9f80 | ||
|
|
4ee2523d59 | ||
|
|
997be18718 | ||
|
|
e966544aeb | ||
|
|
4935c6f258 | ||
|
|
03fafa437c | ||
|
|
0fe7db2694 | ||
|
|
22edb91796 | ||
|
|
5ffc35a955 | ||
|
|
1084fe0425 | ||
|
|
8d867320b0 | ||
|
|
34dd011e90 | ||
|
|
8fce4faeea | ||
|
|
8faff704d5 | ||
|
|
2d71679476 | ||
|
|
715b7a7867 | ||
|
|
8ef28b0683 | ||
|
|
31bdb2eeaa | ||
|
|
63192e2b75 | ||
|
|
fcbbe17909 | ||
|
|
a1e809cbc7 | ||
|
|
93b429cca4 | ||
|
|
3aaa5e74ec | ||
|
|
27156fd215 | ||
|
|
bcac197910 | ||
|
|
336081a78a | ||
|
|
6306bd31e3 | ||
|
|
690e980018 | ||
|
|
f9fbd42962 | ||
|
|
919f6095c6 | ||
|
|
83f62eed79 | ||
|
|
96b971b0da | ||
|
|
05fe018fed | ||
|
|
921c5d636a | ||
|
|
61ae460525 | ||
|
|
caaf2365e0 | ||
|
|
bed9164bcd | ||
|
|
f3621da329 | ||
|
|
2c75582c22 | ||
|
|
aa4f7c3d78 | ||
|
|
9906cf799c | ||
|
|
304937161c | ||
|
|
789497da75 | ||
|
|
14371cd7c6 | ||
|
|
b9dbce47a2 | ||
|
|
eb1672dec3 | ||
|
|
dd0926ebfb | ||
|
|
e80f3639af | ||
|
|
944b3ac07d | ||
|
|
746c22e950 | ||
|
|
a0c85f093c | ||
|
|
728c3b28ed | ||
|
|
03b8df2fe1 | ||
|
|
dae55e4105 | ||
|
|
f8a1ecb644 | ||
|
|
22c2cfb4ea | ||
|
|
b15eaa826b | ||
|
|
32b8d87a64 | ||
|
|
62ba97f517 | ||
|
|
e81a514f29 | ||
|
|
e36ca9f645 | ||
|
|
d9dc21a073 | ||
|
|
59cc1fef67 | ||
|
|
8df8129743 | ||
|
|
b6481e8a68 | ||
|
|
65b65f4d70 | ||
|
|
870ef6bac8 | ||
|
|
f7f0810c17 | ||
|
|
17c3ac1911 | ||
|
|
d74340a317 | ||
|
|
58dc1c67d6 | ||
|
|
7e9e50e52d | ||
|
|
df05e940e8 | ||
|
|
8d6a2fb4f1 | ||
|
|
afa8360397 | ||
|
|
50f8bcec34 | ||
|
|
d13968b753 | ||
|
|
086e3e918e | ||
|
|
e986f0d6e5 | ||
|
|
36ce78bd37 | ||
|
|
e2a11eeb05 | ||
|
|
2d40b86a50 | ||
|
|
4f350631ff | ||
|
|
db67d3840b | ||
|
|
2cd20b79f7 | ||
|
|
b296e9824a | ||
|
|
b0d9c519e1 | ||
|
|
6937193d28 | ||
|
|
500a5eea28 | ||
|
|
9758e1783e | ||
|
|
a57aa2e346 | ||
|
|
4bd5ace2ee | ||
|
|
85f618e16e | ||
|
|
e7995b9cd4 | ||
|
|
6e80c748d2 | ||
|
|
c546c578ce | ||
|
|
59162d93de | ||
|
|
354d22d70d | ||
|
|
f15b2e12d6 | ||
|
|
4ecef0ea11 | ||
|
|
637de8b6fa | ||
|
|
aa6f55d72f | ||
|
|
f018c7f823 | ||
|
|
98e2d009c6 | ||
|
|
09e85ba341 | ||
|
|
f27a1e9504 | ||
|
|
86409687a4 | ||
|
|
ff439c2dbc | ||
|
|
9b6c9b421a | ||
|
|
b5bcdd9df4 | ||
|
|
b36cf84b23 | ||
|
|
fb89b540cd | ||
|
|
86b8c35c04 | ||
|
|
e7497e707e | ||
|
|
b7089bb025 | ||
|
|
612bd70710 | ||
|
|
8affc8abe4 | ||
|
|
d9cf760a25 | ||
|
|
5b6d882d67 | ||
|
|
87ee00c2c5 | ||
|
|
e100287bd8 | ||
|
|
323b8de3dc | ||
|
|
06ec4580be | ||
|
|
efd69a628a | ||
|
|
cc80daec99 | ||
|
|
fd398aa434 | ||
|
|
4b8ea08f4f | ||
|
|
e57cf49dc4 | ||
|
|
111dcbcad4 | ||
|
|
4174896225 | ||
|
|
5738edc1bb | ||
|
|
192bd71c12 | ||
|
|
5ee3b9c878 | ||
|
|
d3020dbd8f | ||
|
|
493575a47b | ||
|
|
fa4febfa93 | ||
|
|
8cd49082ec | ||
|
|
0dbfe7d9e3 | ||
|
|
bbc64e38c7 | ||
|
|
d7188f4b93 | ||
|
|
bb045235bf | ||
|
|
ca4879c496 | ||
|
|
a466a8b253 | ||
|
|
5cd66c1be8 | ||
|
|
4489106bd6 | ||
|
|
485d42373b | ||
|
|
249484a21f | ||
|
|
fb8946254d | ||
|
|
b8a2592737 | ||
|
|
3029949731 | ||
|
|
91061ee669 | ||
|
|
90a9aa3523 | ||
|
|
ad04ccc094 | ||
|
|
a230fe400d | ||
|
|
62ef344a6c | ||
|
|
3eb36768c6 | ||
|
|
b6f79f176c | ||
|
|
e2d151e748 | ||
|
|
54ddd3e025 | ||
|
|
2d7857f45b | ||
|
|
81892491f2 | ||
|
|
0493a9646f |
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
4.1
|
||||
4.2
|
||||
|
||||
235
.travis.yml
235
.travis.yml
@ -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
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@ -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 • [diff](https://github.com/groue/GRDB.swift/compare/v3.6.2...v3.7.20)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
```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 {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@ -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:"...")
|
||||
|
||||
414
Documentation/SQLInterpolation.md
Normal file
414
Documentation/SQLInterpolation.md
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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 = "";
|
||||
|
||||
3
GRDB.xcworkspace/contents.xcworkspacedata
generated
3
GRDB.xcworkspace/contents.xcworkspacedata
generated
@ -10,9 +10,6 @@
|
||||
<FileRef
|
||||
location = "group:JSONSynchronization.playground">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Record.playground">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:TransactionObserver.playground">
|
||||
</FileRef>
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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])! }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>(
|
||||
|
||||
@ -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
|
||||
|
||||
33
GRDB/Core/SQLInterpolation.swift
Normal file
33
GRDB/Core/SQLInterpolation.swift
Normal 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
145
GRDB/Core/SQLLiteral.swift
Normal 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
167
GRDB/Core/SQLRequest.swift
Normal 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
|
||||
@ -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? {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
143
GRDB/Fixit/GRDB-4.0.swift
Normal 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() }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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> { }
|
||||
@ -1,4 +0,0 @@
|
||||
extension DatabaseError {
|
||||
@available(*, unavailable, renamed:"resultCode")
|
||||
public var code: Int32 { return 0 }
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
@ -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 } }
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
135
GRDB/QueryInterface/Association/HasManyThroughAssociation.swift
Normal file
135
GRDB/QueryInterface/Association/HasManyThroughAssociation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
115
GRDB/QueryInterface/Association/HasOneThroughAssociation.swift
Normal file
115
GRDB/QueryInterface/Association/HasOneThroughAssociation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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*.
|
||||
///
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
140
GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift
Normal file
140
GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,6 @@ struct SQLSelectQuery {
|
||||
self.havingExpression = havingExpression
|
||||
self.limit = limit
|
||||
}
|
||||
|
||||
var alias: TableAlias? {
|
||||
return relation.alias
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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: " ")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
387
GRDB/Record/EncodableRecord.swift
Normal file
387
GRDB/Record/EncodableRecord.swift
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: ", ")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user