Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/FTS/FTS5CustomTokenizer.swift
T
2025-01-28 12:28:03 +08:00

173 lines
6.7 KiB
Swift

#if SQLITE_ENABLE_FTS5
/// A type that implements a custom tokenizer for the ``FTS5`` full-text engine.
///
/// See [FTS5 Tokenizers](https://github.com/groue/GRDB.swift/blob/master/Documentation/FTS5Tokenizers.md)
/// for more information.
public protocol FTS5CustomTokenizer: FTS5Tokenizer {
/// The name of the tokenizer.
///
/// The name should uniquely identify the tokenizer: don't use a built-in
/// name such as `ascii`, `porter` or `unicode61`.
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
}
extension FTS5CustomTokenizer {
/// Creates an FTS5 tokenizer descriptor.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
///
/// try 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 {
FTS5TokenizerDescriptor(components: [name] + arguments)
}
}
extension Database {
// MARK: - FTS5
private class FTS5TokenizerConstructor {
let db: Database
let constructor: (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> CInt
init(
db: Database,
constructor: @escaping (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> CInt)
{
self.db = db
self.constructor = constructor
}
}
/// Add a custom FTS5 tokenizer.
///
/// class MyTokenizer : FTS5CustomTokenizer { ... }
/// db.add(tokenizer: MyTokenizer.self)
public func add(tokenizer: (some FTS5CustomTokenizer).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 else {
return SQLITE_ERROR
}
do {
let tokenizer = try tokenizer.init(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 else { return }
Unmanaged<AnyObject>.fromOpaque(constructorPointer).release()
}
func xCreateTokenizer(
constructorPointer: UnsafeMutableRawPointer?,
azArg: UnsafeMutablePointer<UnsafePointer<Int8>?>?,
nArg: CInt,
tokenizerHandle: UnsafeMutablePointer<OpaquePointer?>?)
-> CInt
{
guard let constructorPointer else {
return SQLITE_ERROR
}
let constructor = Unmanaged<FTS5TokenizerConstructor>.fromOpaque(constructorPointer).takeUnretainedValue()
var arguments: [String] = []
if let 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 else { return }
Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).release()
}
func xTokenize(
tokenizerPointer: OpaquePointer?,
context: UnsafeMutableRawPointer?,
flags: CInt,
pText: UnsafePointer<CChar>?,
nText: CInt,
// swiftlint:disable:next line_length
tokenCallback: (@convention(c) (UnsafeMutableRawPointer?, CInt, UnsafePointer<CChar>?, CInt, CInt, CInt) -> CInt)?)
-> CInt
{
guard let tokenizerPointer else {
return SQLITE_ERROR
}
let object = Unmanaged<AnyObject>
.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer))
.takeUnretainedValue()
guard let tokenizer = object as? any 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))
}
}
}
#endif