GBA002/Pods/SQLite.swift/Sources/SQLite/Extensions/FTS4.swift
2017-04-04 15:36:24 -07:00

348 lines
12 KiB
Swift

//
// SQLite.swift
// https://github.com/stephencelis/SQLite.swift
// Copyright © 2014-2015 Stephen Celis.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if SWIFT_PACKAGE
import SQLiteObjc
#endif
extension Module {
public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module {
return FTS4([column] + more)
}
public static func FTS4(_ columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module {
return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer))
}
public static func FTS4(_ config: FTS4Config) -> Module {
return Module(name: "fts4", arguments: config.arguments())
}
}
extension VirtualTable {
/// Builds an expression appended with a `MATCH` query against the given
/// pattern.
///
/// let emails = VirtualTable("emails")
///
/// emails.filter(emails.match("Hello"))
/// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello'
///
/// - Parameter pattern: A pattern to match.
///
/// - Returns: An expression appended with a `MATCH` query against the given
/// pattern.
public func match(_ pattern: String) -> Expression<Bool> {
return "MATCH".infix(tableName(), pattern)
}
public func match(_ pattern: Expression<String>) -> Expression<Bool> {
return "MATCH".infix(tableName(), pattern)
}
public func match(_ pattern: Expression<String?>) -> Expression<Bool?> {
return "MATCH".infix(tableName(), pattern)
}
/// Builds a copy of the query with a `WHERE MATCH` clause.
///
/// let emails = VirtualTable("emails")
///
/// emails.match("Hello")
/// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello'
///
/// - Parameter pattern: A pattern to match.
///
/// - Returns: A query with the given `WHERE MATCH` clause applied.
public func match(_ pattern: String) -> QueryType {
return filter(match(pattern))
}
public func match(_ pattern: Expression<String>) -> QueryType {
return filter(match(pattern))
}
public func match(_ pattern: Expression<String?>) -> QueryType {
return filter(match(pattern))
}
}
public struct Tokenizer {
public static let Simple = Tokenizer("simple")
public static let Porter = Tokenizer("porter")
public static func Unicode61(removeDiacritics: Bool? = nil, tokenchars: Set<Character> = [], separators: Set<Character> = []) -> Tokenizer {
var arguments = [String]()
if let removeDiacritics = removeDiacritics {
arguments.append("removeDiacritics=\(removeDiacritics ? 1 : 0)".quote())
}
if !tokenchars.isEmpty {
let joined = tokenchars.map { String($0) }.joined(separator: "")
arguments.append("tokenchars=\(joined)".quote())
}
if !separators.isEmpty {
let joined = separators.map { String($0) }.joined(separator: "")
arguments.append("separators=\(joined)".quote())
}
return Tokenizer("unicode61", arguments)
}
public static func Custom(_ name: String) -> Tokenizer {
return Tokenizer(Tokenizer.moduleName.quote(), [name.quote()])
}
public let name: String
public let arguments: [String]
fileprivate init(_ name: String, _ arguments: [String] = []) {
self.name = name
self.arguments = arguments
}
fileprivate static let moduleName = "SQLite.swift"
}
extension Tokenizer : CustomStringConvertible {
public var description: String {
return ([name] + arguments).joined(separator: " ")
}
}
extension Connection {
public func registerTokenizer(_ submoduleName: String, next: @escaping (String) -> (String, Range<String.Index>)?) throws {
try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { (
input: UnsafePointer<Int8>, offset: UnsafeMutablePointer<Int32>, length: UnsafeMutablePointer<Int32>) in
let string = String(cString: input)
guard let (token, range) = next(string) else { return nil }
let view = string.utf8
offset.pointee += Int32(string.substring(to: range.lowerBound).utf8.count)
length.pointee = Int32(view.distance(from: range.lowerBound.samePosition(in: view), to: range.upperBound.samePosition(in: view)))
return token
})
}
}
/// Configuration options shared between the [FTS4](https://www.sqlite.org/fts3.html) and
/// [FTS5](https://www.sqlite.org/fts5.html) extensions.
open class FTSConfig {
public enum ColumnOption {
/// [The notindexed= option](https://www.sqlite.org/fts3.html#section_6_5)
case unindexed
}
typealias ColumnDefinition = (Expressible, options: [ColumnOption])
var columnDefinitions = [ColumnDefinition]()
var tokenizer: Tokenizer?
var prefixes = [Int]()
var externalContentSchema: SchemaType?
var isContentless: Bool = false
/// Adds a column definition
@discardableResult open func column(_ column: Expressible, _ options: [ColumnOption] = []) -> Self {
self.columnDefinitions.append((column, options))
return self
}
@discardableResult open func columns(_ columns: [Expressible]) -> Self {
for column in columns {
self.column(column)
}
return self
}
/// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer)
open func tokenizer(_ tokenizer: Tokenizer?) -> Self {
self.tokenizer = tokenizer
return self
}
/// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6)
open func prefix(_ prefix: [Int]) -> Self {
self.prefixes += prefix
return self
}
/// [The content= option](https://www.sqlite.org/fts3.html#section_6_2)
open func externalContent(_ schema: SchemaType) -> Self {
self.externalContentSchema = schema
return self
}
/// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1)
open func contentless() -> Self {
self.isContentless = true
return self
}
func formatColumnDefinitions() -> [Expressible] {
return columnDefinitions.map { $0.0 }
}
func arguments() -> [Expressible] {
return options().arguments
}
func options() -> Options {
var options = Options()
options.append(formatColumnDefinitions())
if let tokenizer = tokenizer {
options.append("tokenize", value: Expression<Void>(literal: tokenizer.description))
}
options.appendCommaSeparated("prefix", values:prefixes.sorted().map { String($0) })
if isContentless {
options.append("content", value: "")
} else if let externalContentSchema = externalContentSchema {
options.append("content", value: externalContentSchema.tableName())
}
return options
}
struct Options {
var arguments = [Expressible]()
@discardableResult mutating func append(_ columns: [Expressible]) -> Options {
arguments.append(contentsOf: columns)
return self
}
@discardableResult mutating func appendCommaSeparated(_ key: String, values: [String]) -> Options {
if values.isEmpty {
return self
} else {
return append(key, value: values.joined(separator: ","))
}
}
@discardableResult mutating func append(_ key: String, value: CustomStringConvertible?) -> Options {
return append(key, value: value?.description)
}
@discardableResult mutating func append(_ key: String, value: String?) -> Options {
return append(key, value: value.map { Expression<String>($0) })
}
@discardableResult mutating func append(_ key: String, value: Expressible?) -> Options {
if let value = value {
arguments.append("=".join([Expression<Void>(literal: key), value]))
}
return self
}
}
}
/// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension.
open class FTS4Config : FTSConfig {
/// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4)
public enum MatchInfo : CustomStringConvertible {
case fts3
public var description: String {
return "fts3"
}
}
/// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options)
public enum Order : CustomStringConvertible {
/// Data structures are optimized for returning results in ascending order by docid (default)
case asc
/// FTS4 stores its data in such a way as to optimize returning results in descending order by docid.
case desc
public var description: String {
switch self {
case .asc: return "asc"
case .desc: return "desc"
}
}
}
var compressFunction: String?
var uncompressFunction: String?
var languageId: String?
var matchInfo: MatchInfo?
var order: Order?
override public init() {
}
/// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1)
open func compress(_ functionName: String) -> Self {
self.compressFunction = functionName
return self
}
/// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1)
open func uncompress(_ functionName: String) -> Self {
self.uncompressFunction = functionName
return self
}
/// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3)
open func languageId(_ columnName: String) -> Self {
self.languageId = columnName
return self
}
/// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4)
open func matchInfo(_ matchInfo: MatchInfo) -> Self {
self.matchInfo = matchInfo
return self
}
/// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options)
open func order(_ order: Order) -> Self {
self.order = order
return self
}
override func options() -> Options {
var options = super.options()
for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) {
options.append("notindexed", value: column)
}
options.append("languageid", value: languageId)
options.append("compress", value: compressFunction)
options.append("uncompress", value: uncompressFunction)
options.append("matchinfo", value: matchInfo)
options.append("order", value: order)
return options
}
}