Skip to content

Commit c7c5cf9

Browse files
committed
Very basic FTS support
This commit provides basic helpers for creating virtual tables with fts4() and for running match queries against them. Still needed: - Custom tokenizer support (and the ability to build with "unicode61" and "icu" tokenizers) - Better support for offsets(), snippet(), matchinfo() - Additional FTS4 options: http://www.sqlite.org/fts3.html#fts4_options Signed-off-by: Stephen Celis <[email protected]>
1 parent 96f4b35 commit c7c5cf9

File tree

5 files changed

+103
-2
lines changed

5 files changed

+103
-2
lines changed

SQLite Tests/FTSTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import XCTest
2+
import SQLite
3+
4+
let subject = Expression<String>("subject")
5+
let body = Expression<String>("body")
6+
7+
class FTSTests: XCTestCase {
8+
9+
let db = Database()
10+
var emails: Query { return db["emails"] }
11+
12+
func test_createVtable_usingFts4_createsVirtualTable() {
13+
ExpectExecution(db, "CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\")", db.create(vtable: emails, using: fts4(subject, body)))
14+
}
15+
16+
func test_createVtable_usingFts4_withPorterTokenizer_createsVirtualTableWithTokenizer() {
17+
ExpectExecution(db, "CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=porter)", db.create(vtable: emails, using: fts4([subject, body], tokenize: .Porter)))
18+
}
19+
20+
func test_match_withColumnExpression_buildsMatchExpressionWithColumnIdentifier() {
21+
db.create(vtable: emails, using: fts4(subject, body))
22+
23+
ExpectExecution(db, "SELECT * FROM \"emails\" WHERE (\"subject\" MATCH 'hello')", emails.filter(match("hello", subject)))
24+
}
25+
26+
func test_match_withQuery_buildsMatchExpressionWithTableIdentifier() {
27+
db.create(vtable: emails, using: fts4(subject, body))
28+
29+
ExpectExecution(db, "SELECT * FROM \"emails\" WHERE (\"emails\" MATCH 'hello')", emails.filter(match("hello", emails)))
30+
}
31+
32+
}

SQLite.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
DC650B9619F0CDC3002FBE91 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC650B9519F0CDC3002FBE91 /* Expression.swift */; };
2323
DCAD429719E2E0F1004A51DF /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429619E2E0F1004A51DF /* Query.swift */; };
2424
DCAD429A19E2EE50004A51DF /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429919E2EE50004A51DF /* QueryTests.swift */; };
25+
DCAFEAD31AABC818000C21A1 /* FTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD21AABC818000C21A1 /* FTS.swift */; };
26+
DCAFEAD41AABC818000C21A1 /* FTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD21AABC818000C21A1 /* FTS.swift */; };
27+
DCAFEAD71AABEFA7000C21A1 /* FTSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */; };
2528
DCC6B36F1A9191C300734B78 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429619E2E0F1004A51DF /* Query.swift */; };
2629
DCC6B3701A9191C300734B78 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743A19C8D6C0004FCF85 /* Statement.swift */; };
2730
DCC6B3711A9191C300734B78 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC650B9519F0CDC3002FBE91 /* Expression.swift */; };
@@ -101,6 +104,8 @@
101104
DCAAE66D19D8A71B00158FEF /* SQLite.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SQLite.playground; sourceTree = "<group>"; };
102105
DCAD429619E2E0F1004A51DF /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Query.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
103106
DCAD429919E2EE50004A51DF /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = "<group>"; };
107+
DCAFEAD21AABC818000C21A1 /* FTS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS.swift; sourceTree = "<group>"; };
108+
DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTSTests.swift; sourceTree = "<group>"; };
104109
DCC6B3801A9191C300734B78 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
105110
DCC6B3921A9191D100734B78 /* SQLiteCipher Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteCipher Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
106111
DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sqlcipher.xcodeproj; path = sqlcipher/sqlcipher.xcodeproj; sourceTree = "<group>"; };
@@ -155,6 +160,7 @@
155160
DC3F170F1A8127A300C83A2F /* Functions.swift */,
156161
DCAD429619E2E0F1004A51DF /* Query.swift */,
157162
DC109CE01A0C4D970070988E /* Schema.swift */,
163+
DCAFEAD21AABC818000C21A1 /* FTS.swift */,
158164
);
159165
name = "Query Building";
160166
sourceTree = "<group>";
@@ -179,6 +185,7 @@
179185
DC3F17121A814F7000C83A2F /* FunctionsTests.swift */,
180186
DCAD429919E2EE50004A51DF /* QueryTests.swift */,
181187
DC109CE31A0C4F5D0070988E /* SchemaTests.swift */,
188+
DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */,
182189
DC37740319C8CBB3004FCF85 /* Supporting Files */,
183190
);
184191
path = "SQLite Tests";
@@ -466,6 +473,7 @@
466473
buildActionMask = 2147483647;
467474
files = (
468475
DC37743519C8D626004FCF85 /* Database.swift in Sources */,
476+
DCAFEAD31AABC818000C21A1 /* FTS.swift in Sources */,
469477
DC37743B19C8D6C0004FCF85 /* Statement.swift in Sources */,
470478
DC37743819C8D693004FCF85 /* Value.swift in Sources */,
471479
DC650B9619F0CDC3002FBE91 /* Expression.swift in Sources */,
@@ -481,6 +489,7 @@
481489
buildActionMask = 2147483647;
482490
files = (
483491
DCF37F8819DDAF79001534AA /* TestHelper.swift in Sources */,
492+
DCAFEAD71AABEFA7000C21A1 /* FTSTests.swift in Sources */,
484493
DCF37F8219DDAC2D001534AA /* DatabaseTests.swift in Sources */,
485494
DCF37F8519DDAF3F001534AA /* StatementTests.swift in Sources */,
486495
DC475EA219F219AF00788FBD /* ExpressionTests.swift in Sources */,
@@ -501,6 +510,7 @@
501510
DCC6B36F1A9191C300734B78 /* Query.swift in Sources */,
502511
DCC6B3741A9191C300734B78 /* Schema.swift in Sources */,
503512
DCC6B3A91A91975C00734B78 /* Functions.swift in Sources */,
513+
DCAFEAD41AABC818000C21A1 /* FTS.swift in Sources */,
504514
DCC6B3751A9191C300734B78 /* SQLite-Bridging.c in Sources */,
505515
DCC6B3A41A9194A800734B78 /* Cipher.swift in Sources */,
506516
);

SQLite/Expression.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public struct Expression<T> {
6060
/// Builds a SQL expression with the given value.
6161
///
6262
/// :param: binding A raw SQL value.
63-
private init(binding: Binding?) {
63+
internal init(binding: Binding?) {
6464
self.init(literal: "?", [binding])
6565
}
6666

@@ -966,7 +966,7 @@ internal func wrap<T, U>(function: String, expression: Expression<T>) -> Express
966966
return Expression(literal: "\(function)\(surround(expression.SQL))", expression.bindings)
967967
}
968968

969-
private func infix<T, U, V>(function: String, lhs: Expression<T>, rhs: Expression<U>) -> Expression<V> {
969+
internal func infix<T, U, V>(function: String, lhs: Expression<T>, rhs: Expression<U>) -> Expression<V> {
970970
return Expression(literal: surround("\(lhs.SQL) \(function) \(rhs.SQL)"), lhs.bindings + rhs.bindings)
971971
}
972972

SQLite/FTS.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// SQLite.swift
3+
// https://github.com/stephencelis/SQLite.swift
4+
// Copyright (c) 2014-2015 Stephen Celis.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
//
24+
25+
public func fts4(columns: Expression<String>...) -> Expression<()> {
26+
return fts4(columns)
27+
}
28+
29+
// TODO: matchinfo, compress, uncompress
30+
public func fts4(columns: [Expression<String>], tokenize tokenizer: Tokenizer? = nil) -> Expression<()> {
31+
var options = [String: String]()
32+
options["tokenize"] = tokenizer?.rawValue
33+
return fts("fts4", columns, options)
34+
}
35+
36+
private func fts(function: String, columns: [Expression<String>], options: [String: String]) -> Expression<()> {
37+
var definitions: [Expressible] = columns.map { $0.expression }
38+
for (key, value) in options {
39+
definitions.append(Expression<()>(literal: "\(key)=\(value)"))
40+
}
41+
return wrap(function, Expression<()>.join(", ", definitions))
42+
}
43+
44+
public enum Tokenizer: String {
45+
46+
case Simple = "simple"
47+
48+
case Porter = "porter"
49+
50+
}
51+
52+
public func match(string: String, expression: Query) -> Expression<Bool> {
53+
return infix("MATCH", Expression<String>(expression.tableName), Expression<String>(binding: string))
54+
}

SQLite/Schema.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public extension Database {
4343
return run("\(create) AS \(expression.SQL)", expression.bindings)
4444
}
4545
46+
public func create(#vtable: Query, ifNotExists: Bool = false, using: Expression<()>) -> Statement {
47+
let create = createSQL("VIRTUAL TABLE", false, false, ifNotExists, vtable.tableName.unaliased.SQL)
48+
return run("\(create) USING \(using.SQL)")
49+
}
50+
4651
public func rename(table tableName: String, to table: Query) -> Statement {
4752
return run("ALTER TABLE \(quote(identifier: tableName)) RENAME TO \(table.tableName.unaliased.SQL)")
4853
}

0 commit comments

Comments
 (0)