-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathFtsSetup.swift
184 lines (159 loc) · 6.73 KB
/
FtsSetup.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//
// FtsSetup.swift
// PowerSyncExample
//
// Created by Wade Morris on 4/9/25.
//
import Foundation
import PowerSync
enum ExtractType {
case columnOnly
case columnInOperation
}
/// Generates SQL JSON extract expressions for FTS triggers.
///
/// - Parameters:
/// - type: The type of extraction needed (`columnOnly` or `columnInOperation`).
/// - sourceColumn: The JSON source column (e.g., `'data'`, `'NEW.data'`).
/// - columns: The list of column names to extract.
/// - Returns: A comma-separated string of SQL expressions.
func generateJsonExtracts(type: ExtractType, sourceColumn: String, columns: [String]) -> String {
func createExtract(jsonSource: String, columnName: String) -> String {
return "json_extract(\(jsonSource), '$.\"\(columnName)\"')"
}
func generateSingleColumnSql(columnName: String) -> String {
switch type {
case .columnOnly:
return createExtract(jsonSource: sourceColumn, columnName: columnName)
case .columnInOperation:
return "\"\(columnName)\" = \(createExtract(jsonSource: sourceColumn, columnName: columnName))"
}
}
return columns.map(generateSingleColumnSql).joined(separator: ", ")
}
/// Generates the SQL statements required to set up an FTS5 virtual table
/// and corresponding triggers for a given PowerSync table.
///
///
/// - Parameters:
/// - tableName: The public name of the table to index (e.g., "lists", "todos").
/// - columns: The list of column names within the table to include in the FTS index.
/// - schema: The PowerSync `Schema` object to find the internal table name.
/// - tokenizationMethod: The FTS5 tokenization method (e.g., "porter unicode61", "unicode61").
/// - Returns: An array of SQL statements to be executed, or `nil` if the table is not found in the schema.
func getFtsSetupSqlStatements(
tableName: String,
columns: [String],
schema: Schema,
tokenizationMethod: String = "unicode61"
) -> [String]? {
guard let internalName = schema.tables.first(where: { $0.name == tableName })?.internalName else {
print("Table '\(tableName)' not found in schema. Skipping FTS setup for this table.")
return nil
}
let ftsTableName = "fts_\(tableName)"
let stringColumnsForCreate = columns.map { "\"\($0)\"" }.joined(separator: ", ")
let stringColumnsForInsertList = columns.map { "\"\($0)\"" }.joined(separator: ", ")
var sqlStatements: [String] = []
// 1. Create the FTS5 Virtual Table
sqlStatements.append("""
CREATE VIRTUAL TABLE IF NOT EXISTS \(ftsTableName)
USING fts5(id UNINDEXED, \(stringColumnsForCreate), tokenize='\(tokenizationMethod)');
""")
// 2. Copy existing data from the main table to the FTS table
sqlStatements.append("""
INSERT INTO \(ftsTableName)(rowid, id, \(stringColumnsForInsertList))
SELECT rowid, id, \(generateJsonExtracts(type: .columnOnly, sourceColumn: "data", columns: columns))
FROM \(internalName);
""")
// 3. Create INSERT Trigger
sqlStatements.append("""
CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_\(tableName) AFTER INSERT ON \(internalName)
BEGIN
INSERT INTO \(ftsTableName)(rowid, id, \(stringColumnsForInsertList))
VALUES (
NEW.rowid,
NEW.id,
\(generateJsonExtracts(type: .columnOnly, sourceColumn: "NEW.data", columns: columns))
);
END;
""")
// 4. Create UPDATE Trigger
sqlStatements.append("""
CREATE TRIGGER IF NOT EXISTS fts_update_trigger_\(tableName) AFTER UPDATE ON \(internalName)
BEGIN
UPDATE \(ftsTableName)
SET \(generateJsonExtracts(type: .columnInOperation, sourceColumn: "NEW.data", columns: columns))
WHERE rowid = NEW.rowid;
END;
""")
// 5. Create DELETE Trigger
sqlStatements.append("""
CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_\(tableName) AFTER DELETE ON \(internalName)
BEGIN
DELETE FROM \(ftsTableName) WHERE rowid = OLD.rowid;
END;
""")
return sqlStatements
}
/// Configures Full-Text Search (FTS) tables and triggers for specified tables
/// within the PowerSync database. Call this function during database initialization.
///
/// Executes all generated SQL within a single transaction.
///
/// - Parameters:
/// - db: The initialized `PowerSyncDatabaseProtocol` instance.
/// - schema: The `Schema` instance matching the database.
/// - Throws: An error if the database transaction fails.
func configureFts(db: PowerSyncDatabaseProtocol, schema: Schema) async throws {
let ftsCheckTable = "fts_\(LISTS_TABLE)"
let checkSql = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
do {
let existingTable: String? = try await db.getOptional(sql: checkSql, parameters: [ftsCheckTable]) { cursor in
try cursor.getString(name: "name")
}
if existingTable != nil {
print("[FTS] FTS table '\(ftsCheckTable)' already exists. Skipping setup.")
return
}
} catch {
print("[FTS] Failed to check for existing FTS tables: \(error.localizedDescription). Proceeding with setup attempt.")
}
print("[FTS] Starting FTS configuration...")
var allSqlStatements: [String] = []
if let listStatements = getFtsSetupSqlStatements(
tableName: LISTS_TABLE,
columns: ["name"],
schema: schema,
tokenizationMethod: "porter unicode61"
) {
print("[FTS] Generated \(listStatements.count) SQL statements for '\(LISTS_TABLE)' table.")
allSqlStatements.append(contentsOf: listStatements)
}
if let todoStatements = getFtsSetupSqlStatements(
tableName: TODOS_TABLE,
columns: ["description"],
schema: schema
) {
print("[FTS] Generated \(todoStatements.count) SQL statements for '\(TODOS_TABLE)' table.")
allSqlStatements.append(contentsOf: todoStatements)
}
// --- Execute all generated SQL statements ---
if !allSqlStatements.isEmpty {
do {
print("[FTS] Executing \(allSqlStatements.count) SQL statements in a transaction...")
_ = try await db.writeTransaction { transaction in
for sql in allSqlStatements {
print("[FTS] Executing SQL:\n\(sql)")
_ = try transaction.execute(sql: sql, parameters: [])
}
}
print("[FTS] Configuration completed successfully.")
} catch {
print("[FTS] Error during FTS setup SQL execution: \(error.localizedDescription)")
throw error
}
} else {
print("[FTS] No FTS SQL statements were generated. Check table names and schema definition.")
}
}