Skip to content

Expose full FTS4/5 configuration #435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Documentation/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,22 @@ try db.run(emails.create(.FTS4([subject, body], tokenize: .Porter)))
// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter)
```

We can set the full range of parameters by creating a `FTS4Config` object.

``` swift
let emails = VirtualTable("emails")
let subject = Expression<String>("subject")
let body = Expression<String>("body")
let config = FTS4Config()
.column(subject)
.column(body, [.unindexed])
.languageId("lid")
.order(.Desc)

try db.run(emails.create(.FTS4(config))
// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", notindexed="body", languageid="lid", order="desc")
```

Once we insert a few rows, we can search using the `match` function, which takes a table or column as its first argument and a query string as its second.

``` swift
Expand All @@ -1414,6 +1430,22 @@ let replies = emails.filter(subject.match("Re:*"))
// SELECT * FROM "emails" WHERE "subject" MATCH 'Re:*'
```

### FTS5

When linking against a version of SQLite with [FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual table
in a similar fashion.

```swift
let emails = VirtualTable("emails")
let subject = Expression<String>("subject")
let body = Expression<String>("body")
let config = FTS5Config()
.column(subject)
.column(body, [.unindexed])

try db.run(emails.create(.FTS5(config))
// CREATE VIRTUAL TABLE "emails" USING fts5("subject", "body" UNINDEXED)
```

## Executing Arbitrary SQL

Expand Down
16 changes: 16 additions & 0 deletions SQLite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
03A65E941C6BB3030062603F /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; };
03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; };
03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; };
19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; };
19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; };
19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; };
19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; };
19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; };
19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; };
EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; };
EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; };
EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; };
Expand Down Expand Up @@ -154,6 +160,8 @@
03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
03A65E961C6BB3210062603F /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; };
19A1721B8984686B9963B45D /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = "<group>"; };
19A1730E4390C775C25677D1 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = "<group>"; };
39548A631CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
39548A651CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
39548A671CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
Expand Down Expand Up @@ -406,6 +414,7 @@
EE247B331C3F142E00AE3E12 /* ValueTests.swift */,
EE247B161C3F127200AE3E12 /* TestHelpers.swift */,
EE247AE41C3F04ED00AE3E12 /* Info.plist */,
19A1721B8984686B9963B45D /* FTS5Tests.swift */,
);
path = SQLiteTests;
sourceTree = "<group>";
Expand All @@ -429,6 +438,7 @@
children = (
EE247AF51C3F06E900AE3E12 /* FTS4.swift */,
EE247AF61C3F06E900AE3E12 /* R*Tree.swift */,
19A1730E4390C775C25677D1 /* FTS5.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -786,6 +796,7 @@
03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */,
03A65E771C6BB2E60062603F /* Connection.swift in Sources */,
03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */,
19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -808,6 +819,7 @@
03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */,
03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */,
03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */,
19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -840,6 +852,7 @@
EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */,
EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */,
EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */,
19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -862,6 +875,7 @@
EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */,
EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */,
EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */,
19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -887,6 +901,7 @@
EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */,
EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */,
EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */,
19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -909,6 +924,7 @@
EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */,
EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */,
EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */,
19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
196 changes: 190 additions & 6 deletions SQLite/Extensions/FTS4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,12 @@ extension Module {
}

@warn_unused_result public static func FTS4(columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module {
var columns = columns

if let tokenizer = tokenizer {
columns.append("=".join([Expression<Void>(literal: "tokenize"), Expression<Void>(literal: tokenizer.description)]))
}
return Module(name: "fts4", arguments: columns)
return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer))
}

@warn_unused_result public static func FTS4(config: FTS4Config) -> Module {
return Module(name: "fts4", arguments: config.arguments())
}
}

extension VirtualTable {
Expand Down Expand Up @@ -156,3 +154,189 @@ extension Connection {
}

}

/// Configuration options shared between the [FTS4](https://www.sqlite.org/fts3.html) and
/// [FTS5](https://www.sqlite.org/fts5.html) extensions.
public 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
public func column(column: Expressible, _ options: [ColumnOption] = []) -> Self {
self.columnDefinitions.append((column, options))
return self
}

public func columns(columns: [Expressible]) -> Self {
for column in columns {
self.column(column)
}
return self
}

/// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer)
public func tokenizer(tokenizer: Tokenizer?) -> Self {
self.tokenizer = tokenizer
return self
}

/// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6)
public func prefix(prefix: [Int]) -> Self {
self.prefixes += prefix
return self
}

/// [The content= option](https://www.sqlite.org/fts3.html#section_6_2)
public func externalContent(schema: SchemaType) -> Self {
self.externalContentSchema = schema
return self
}

/// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1)
public 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.sort().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]()

mutating func append(columns: [Expressible]) -> Options {
arguments.appendContentsOf(columns)
return self
}

mutating func appendCommaSeparated(key: String, values: [String]) -> Options {
if values.isEmpty {
return self
} else {
return append(key, value: values.joinWithSeparator(","))
}
}

mutating func append(key: String, value: CustomStringConvertible?) -> Options {
return append(key, value: value?.description)
}

mutating func append(key: String, value: String?) -> Options {
return append(key, value: value.map { Expression<String>($0) })
}

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.
public 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)
public func compress(functionName: String) -> Self {
self.compressFunction = functionName
return self
}

/// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1)
public func uncompress(functionName: String) -> Self {
self.uncompressFunction = functionName
return self
}

/// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3)
public func languageId(columnName: String) -> Self {
self.languageId = columnName
return self
}

/// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4)
public func matchInfo(matchInfo: MatchInfo) -> Self {
self.matchInfo = matchInfo
return self
}

/// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options)
public 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
}
}
Loading