Files

377 lines
17 KiB
Swift

#if SQLITE_ENABLE_FTS5
import XCTest
import GRDB
class FTS5TableBuilderTests: GRDBTestCase {
func testWithoutBody() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content)")
try db.execute(sql: "INSERT INTO documents VALUES (?)", arguments: ["abc"])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM documents WHERE documents MATCH ?", arguments: ["abc"])!, 1)
}
}
func testOptions() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", ifNotExists: true, using: FTS5()) { t in
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE IF NOT EXISTS \"documents\" USING fts5(content)")
try db.execute(sql: "INSERT INTO documents VALUES (?)", arguments: ["abc"])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM documents WHERE documents MATCH ?", arguments: ["abc"])!, 1)
}
}
func testAsciiTokenizer() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .ascii()
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''ascii''')")
}
}
func testAsciiTokenizerSeparators() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .ascii(separators: ["X"])
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''ascii'' ''separators'' ''X''')")
}
}
func testAsciiTokenizerTokenCharacters() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .ascii(tokenCharacters: Set(".-"))
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''ascii'' ''tokenchars'' ''-.''')")
}
}
func testDefaultPorterTokenizer() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .porter()
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''porter''')")
}
}
func testPorterOnAsciiTokenizer() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .porter(wrapping: .ascii())
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''porter'' ''ascii''')")
}
}
func testPorterOnUnicode61Tokenizer() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .porter(wrapping: .unicode61())
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''porter'' ''unicode61''')")
}
}
func testUnicode61Tokenizer() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61()
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61''')")
}
}
func testUnicode61TokenizerDiacriticsKeep() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61(diacritics: .keep)
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61'' ''remove_diacritics'' ''0''')")
}
}
#if GRDBCUSTOMSQLITE
func testUnicode61TokenizerDiacriticsRemove() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61(diacritics: .remove)
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61'' ''remove_diacritics'' ''2''')")
}
}
#elseif !GRDBCIPHER
func testUnicode61TokenizerDiacriticsRemove() throws {
guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else {
throw XCTSkip()
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61(diacritics: .remove)
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61'' ''remove_diacritics'' ''2''')")
}
}
#endif
func testUnicode61TokenizerSeparators() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61(separators: ["X"])
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61'' ''separators'' ''X''')")
}
}
func testUnicode61TokenizerTokenCharacters() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.tokenizer = .unicode61(tokenCharacters: Set(".-"))
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, tokenize='''unicode61'' ''tokenchars'' ''-.''')")
}
}
func testColumns() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "books", using: FTS5()) { t in
t.column("author")
t.column("title")
t.column("body")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"books\" USING fts5(author, title, body)")
try db.execute(sql: "INSERT INTO books VALUES (?, ?, ?)", arguments: ["Melville", "Moby Dick", "Call me Ishmael."])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["Melville"])!, 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["title:Melville"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["author:Melville"])!, 1)
}
}
func testNotIndexedColumns() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "books", using: FTS5()) { t in
t.column("author").notIndexed()
t.column("title")
t.column("body").notIndexed()
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"books\" USING fts5(author UNINDEXED, title, body UNINDEXED)")
try db.execute(sql: "INSERT INTO books VALUES (?, ?, ?)", arguments: ["Melville", "Moby Dick", "Call me Ishmael."])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["Dick"])!, 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["title:Dick"])!, 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["author:Dick"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["Melville"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["title:Melville"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM books WHERE books MATCH ?", arguments: ["author:Melville"])!, 0)
}
}
func testFTS5Options() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(virtualTable: "documents", using: FTS5()) { t in
t.content = ""
t.prefixes = [2, 4]
t.columnSize = 0
t.detail = "column"
t.column("content")
}
assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts5(content, content='', prefix='2 4', columnSize=0, detail=column)")
}
}
func testFTS5Synchronization() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "documents") { t in
t.primaryKey("id", .integer)
t.column("content", .text)
}
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
try db.create(virtualTable: "ft_documents", using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
// Prepopulated
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["foo"])!, 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["bar"])!, 0)
// Synchronized on update
try db.execute(sql: "UPDATE documents SET content = ?", arguments: ["bar"])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["foo"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["bar"])!, 1)
// Synchronized on insert
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["foo"])!, 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["bar"])!, 1)
// Synchronized on delete
try db.execute(sql: "DELETE FROM documents WHERE content = ?", arguments: ["foo"])
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["foo"])!, 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ft_documents WHERE ft_documents MATCH ?", arguments: ["bar"])!, 1)
}
}
func testFTS5SynchronizationIfNotExists() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
try db.create(table: "documents") { t in
t.primaryKey("id", .integer)
t.column("content", .text)
}
assertDidExecute(sql: "CREATE TABLE \"documents\" (\"id\" INTEGER PRIMARY KEY, \"content\" TEXT)")
try db.create(virtualTable: "ft_documents", ifNotExists: true, using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
assertDidExecute(sql: """
CREATE VIRTUAL TABLE IF NOT EXISTS "ft_documents" USING fts5(content, content='documents', content_rowid='id')
""")
assertDidExecute(sql: """
CREATE TRIGGER IF NOT EXISTS "__ft_documents_ai" AFTER INSERT ON "documents" BEGIN
INSERT INTO "ft_documents"("rowid", "content") VALUES (new."id", new."content");
END
""")
}
}
func testFTS5SynchronizationCleanup() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "documents") { t in
t.primaryKey("id", .integer)
t.column("content", .text)
}
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
try db.create(virtualTable: "ft_documents", using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
try db.drop(table: "ft_documents")
try db.dropFTS5SynchronizationTriggers(forTable: "ft_documents")
// It is possible to modify the content table
try db.execute(sql: "UPDATE documents SET content = ?", arguments: ["bar"])
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
try db.execute(sql: "DELETE FROM documents WHERE content = ?", arguments: ["foo"])
// It is possible to recreate the FT table
try db.create(virtualTable: "ft_documents", using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
}
}
func testFTS5SynchronizationCleanupWithLegacySupport() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "documents") { t in
t.primaryKey("id", .integer)
t.column("content", .text)
}
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
try db.create(virtualTable: "ft_documents", using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
try db.drop(table: "ft_documents")
try db.dropFTS5SynchronizationTriggers(forTable: "documents") // legacy GRDB <= 2.3.1
try db.dropFTS5SynchronizationTriggers(forTable: "ft_documents")
// It is possible to modify the content table
try db.execute(sql: "UPDATE documents SET content = ?", arguments: ["bar"])
try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["foo"])
try db.execute(sql: "DELETE FROM documents WHERE content = ?", arguments: ["foo"])
// It is possible to recreate the FT table
try db.create(virtualTable: "ft_documents", using: FTS5()) { t in
t.synchronize(withTable: "documents")
t.column("content")
}
}
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1390>
func testIssue1390() throws {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
guard sqlite3_libversion_number() >= 3035000 else {
throw XCTSkip("UPSERT is not available")
}
#else
guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else {
throw XCTSkip("UPSERT is not available")
}
#endif
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
struct Content: Codable, Hashable, PersistableRecord {
var id: String
var text: String
}
try db.create(table: "content") { t in
t.primaryKey("id", .text)
t.column("text", .text)
}
try db.create(virtualTable: "fts", using: FTS5()) { t in
t.tokenizer = .porter()
t.synchronize(withTable: "content")
t.column("id").notIndexed()
t.column("text")
}
try db.inTransaction {
try Content(id: "foobar", text: "baz").upsert(db)
return .commit
}
}
}
}
#endif