Skip to content

Commit a23d2cd

Browse files
committed
Full-text search
1 parent a2a7c06 commit a23d2cd

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
public protocol FTS5: Table {}
2+
3+
extension TableDefinition where QueryValue: FTS5 {
4+
public var rank: some QueryExpression<Double?> {
5+
SQLQueryExpression(
6+
"""
7+
\(QueryValue.self)."rank"
8+
"""
9+
)
10+
}
11+
12+
public func match(_ pattern: some QueryExpression<String>) -> some QueryExpression<Bool> {
13+
SQLQueryExpression(
14+
"""
15+
(\(QueryValue.self) MATCH \(pattern))
16+
"""
17+
)
18+
}
19+
}
20+
21+
extension TableColumnExpression where Root: FTS5 {
22+
public func highlight(
23+
_ open: String,
24+
_ close: String
25+
) -> some QueryExpression<String> {
26+
SQLQueryExpression(
27+
"""
28+
highlight(\
29+
\(Root.self), \
30+
(\
31+
SELECT "cid" FROM pragma_table_info(\(quote: Root.tableName, delimiter: .text)) \
32+
WHERE "name" = \(quote: name, delimiter: .text)\
33+
),
34+
\(quote: open, delimiter: .text), \
35+
\(quote: close, delimiter: .text)\
36+
)
37+
"""
38+
)
39+
}
40+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
import InlineSnapshotTesting
3+
import StructuredQueries
4+
import StructuredQueriesTestSupport
5+
import Testing
6+
7+
extension SnapshotTests {
8+
@Suite struct FTSTests {
9+
@Test func basics() {
10+
assertQuery(
11+
ReminderText
12+
.where { $0.match("take OR apple") }
13+
.order(by: \.rank)
14+
.select { ($0.title.highlight("**", "**"), $0.notes.highlight("**", "**")) }
15+
) {
16+
"""
17+
SELECT highlight("reminderTexts", (SELECT "cid" FROM pragma_table_info('reminderTexts') WHERE "name" = 'title'),
18+
'**', '**'), highlight("reminderTexts", (SELECT "cid" FROM pragma_table_info('reminderTexts') WHERE "name" = 'notes'),
19+
'**', '**')
20+
FROM "reminderTexts"
21+
WHERE ("reminderTexts" MATCH 'take OR apple')
22+
ORDER BY "reminderTexts"."rank"
23+
"""
24+
}results: {
25+
"""
26+
┌──────────────────────┬──────────────────────────┐
27+
"Groceries""Milk, Eggs, **Apple**s"
28+
"**Take** out trash"""
29+
"**Take** a walk"""
30+
└──────────────────────┴──────────────────────────┘
31+
"""
32+
}
33+
}
34+
35+
@Test func unranked() {
36+
assertQuery(
37+
ReminderText
38+
.select { ($0.listTitle, $0.rank) }
39+
.limit(1)
40+
) {
41+
"""
42+
SELECT "reminderTexts"."listTitle", "reminderTexts"."rank"
43+
FROM "reminderTexts"
44+
LIMIT 1
45+
"""
46+
}results: {
47+
"""
48+
┌────────────┬─────┐
49+
"Personal" │ nil │
50+
└────────────┴─────┘
51+
"""
52+
}
53+
}
54+
}
55+
}

Tests/StructuredQueriesTests/Support/Schema.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,21 @@ struct ReminderTag: Equatable {
7373
var title = ""
7474
}
7575

76+
@Table
77+
struct ReminderText: FTS5 {
78+
let reminderID: Reminder.ID
79+
let title: String
80+
let notes: String
81+
let listID: RemindersList.ID
82+
let listTitle: String
83+
let tags: String
84+
}
85+
7686
extension Database {
7787
static func `default`() throws -> Database {
7888
let db = try Database()
7989
try db.migrate()
90+
try db.installTriggers()
8091
try db.seedDatabase()
8192
return db
8293
}
@@ -152,6 +163,101 @@ extension Database {
152163
)
153164
"""
154165
)
166+
try execute(
167+
"""
168+
CREATE VIRTUAL TABLE "reminderTexts" USING fts5(
169+
"reminderID" UNINDEXED,
170+
"title",
171+
"notes",
172+
"listID" UNINDEXED,
173+
"listTitle",
174+
"tags",
175+
tokenize = 'trigram'
176+
)
177+
"""
178+
)
179+
}
180+
181+
func installTriggers() throws {
182+
try execute(
183+
Reminder.createTemporaryTrigger(
184+
after: .insert { new in
185+
ReminderText.insert {
186+
($0.reminderID, $0.title, $0.notes, $0.listID, $0.listTitle, $0.tags)
187+
} select: {
188+
Reminder
189+
.find(new.id)
190+
.join(RemindersList.all) { $0.remindersListID.eq($1.id) }
191+
.select { ($0.id, $0.title, $0.notes, $1.id, $1.title, "") }
192+
}
193+
}
194+
)
195+
)
196+
try execute(
197+
Reminder.createTemporaryTrigger(
198+
after: .update {
199+
($0.title, $0.notes, $0.remindersListID)
200+
} forEachRow: { _, new in
201+
ReminderText
202+
.where { $0.reminderID.eq(new.id) }
203+
.update {
204+
$0.title = new.title
205+
$0.notes = new.notes
206+
$0.listID = new.remindersListID
207+
}
208+
}
209+
)
210+
)
211+
try execute(
212+
Reminder.createTemporaryTrigger(
213+
after: .delete { old in
214+
ReminderText
215+
.where { $0.reminderID.eq(old.id) }
216+
.delete()
217+
}
218+
)
219+
)
220+
try execute(
221+
RemindersList.createTemporaryTrigger(
222+
after: .update {
223+
$0.title
224+
} forEachRow: { _, new in
225+
ReminderText
226+
.where { $0.listID.eq(new.id) }
227+
.update { $0.listTitle = new.title }
228+
}
229+
)
230+
)
231+
try execute(
232+
ReminderTag.createTemporaryTrigger(
233+
after: .insert { new in
234+
ReminderText
235+
.where { $0.reminderID.eq(new.reminderID) }
236+
.update {
237+
$0.tags =
238+
ReminderTag
239+
.where { $0.reminderID.eq(new.reminderID) }
240+
.join(Tag.all) { $0.tagID.eq($1.id) }
241+
.select { $1.title.groupConcat(" ") ?? "" }
242+
}
243+
}
244+
)
245+
)
246+
try execute(
247+
ReminderTag.createTemporaryTrigger(
248+
after: .delete { old in
249+
ReminderText
250+
.where { $0.reminderID.eq(old.reminderID) }
251+
.update {
252+
$0.tags =
253+
ReminderTag
254+
.where { $0.reminderID.eq(old.reminderID) }
255+
.join(Tag.all) { $0.tagID.eq($1.id) }
256+
.select { $1.title.groupConcat(" ") ?? "" }
257+
}
258+
}
259+
)
260+
)
155261
}
156262

157263
func seedDatabase() throws {

0 commit comments

Comments
 (0)