diff --git a/Documentation/Index.md b/Documentation/Index.md index 5d72aa4b..a03c26a3 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -64,6 +64,7 @@ - [Other Operators](#other-operators) - [Core SQLite Functions](#core-sqlite-functions) - [Aggregate SQLite Functions](#aggregate-sqlite-functions) + - [Window SQLite Functions](#window-sqlite-functions) - [Date and Time Functions](#date-and-time-functions) - [Custom SQL Functions](#custom-sql-functions) - [Custom Collations](#custom-collations) @@ -1871,6 +1872,11 @@ Most of SQLite’s [aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been surfaced in and type-audited for SQLite.swift. +## Window SQLite Functions + +Most of SQLite's [window functions](https://www.sqlite.org/windowfunctions.html) have been +surfaced in and type-audited for SQLite.swift. Currently only `OVER (ORDER BY ...)` windowing is possible. + ## Date and Time functions SQLite's [date and time](https://www.sqlite.org/lang_datefunc.html) diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index d23b6d64..cb1aba81 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -195,6 +195,13 @@ 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; 997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; 997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; @@ -281,6 +288,9 @@ DEB307092B61CF9500F9D46B /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; DEB3070D2B61CF9500F9D46B /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + DBB93D5A2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5B2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5C2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.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 */; }; @@ -412,6 +422,8 @@ 3DF7B79B2884C901005DD8CA /* Planning.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Planning.md; sourceTree = ""; }; 3DFC0B862886C239001C8FC9 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; + 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctions.swift; sourceTree = ""; }; + 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctionsTests.swift; sourceTree = ""; }; 997DF2AD287FC06D00F8DF95 /* Query+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+with.swift"; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB58B21028FB864300F8EEA4 /* SchemaReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReader.swift; sourceTree = ""; }; @@ -420,6 +432,7 @@ DEB306E52B61CEF500F9D46B /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests visionOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "SQLite visionOS.xctestplan"; path = "Tests/SQLite visionOS.xctestplan"; sourceTree = ""; }; + DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReaderTests.swift; sourceTree = ""; }; EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -572,6 +585,7 @@ 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */, 19A1709D5BDD2691BA160012 /* SetterTests.swift */, 19A174FE5B47A97937A27276 /* RowTests.swift */, + 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */, ); path = Typed; sourceTree = ""; @@ -579,6 +593,7 @@ 19A17B56FBA20E7245BC8AC0 /* Schema */ = { isa = PBXGroup; children = ( + DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */, 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */, 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */, 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */, @@ -706,6 +721,7 @@ isa = PBXGroup; children = ( EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */, + 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */, EE247AFB1C3F06E900AE3E12 /* Collation.swift */, EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */, EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */, @@ -1120,6 +1136,7 @@ 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */, 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */, 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */, + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */, 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */, 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */, @@ -1158,6 +1175,7 @@ 19A17FACE8E4D54A50BA934E /* FTS5Tests.swift in Sources */, 19A177909023B7B940C5805E /* FTSIntegrationTests.swift in Sources */, 19A17E1DD976D5CE80018749 /* FTS4Tests.swift in Sources */, + DBB93D5C2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, 19A17411403D60640467209E /* ExpressionTests.swift in Sources */, 19A17CA4D7B63D845428A9C5 /* StatementTests.swift in Sources */, 19A17885B646CB0201BE4BD5 /* QueryTests.swift in Sources */, @@ -1174,6 +1192,7 @@ 19A17746150A815944A6820B /* SelectTests.swift in Sources */, 19A1766135CE9786B1878603 /* ValueTests.swift in Sources */, 19A177D5C6542E2D572162E5 /* QueryIntegrationTests.swift in Sources */, + 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */, 19A178DF5A96CFEFF1E271F6 /* AggregateFunctionsTests.swift in Sources */, 19A17437659BD7FD787D94A6 /* CustomAggregationTests.swift in Sources */, 19A17F907258E524B3CA2FAE /* SetterTests.swift in Sources */, @@ -1217,6 +1236,7 @@ 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */, 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */, 19A17DF8D4F13A20F5D2269E /* Result.swift in Sources */, + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */, 19A17DFE05ED8B1F7C45F7EE /* SchemaChanger.swift in Sources */, 19A17D1BEABA610ABF003D67 /* SchemaDefinitions.swift in Sources */, 19A17A33EA026C2E2CEBAF36 /* Connection+Schema.swift in Sources */, @@ -1321,6 +1341,7 @@ EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */, 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */, + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */, EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */, @@ -1359,6 +1380,7 @@ 19A178DA2BB5970778CCAF13 /* FTS5Tests.swift in Sources */, 19A1755C49154C87304C9146 /* FTSIntegrationTests.swift in Sources */, 19A17444861E1443143DEB44 /* FTS4Tests.swift in Sources */, + DBB93D5A2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, 19A17DD33C2E43DD6EE05A60 /* ExpressionTests.swift in Sources */, 19A17D6EC40BC35A5DC81BA8 /* StatementTests.swift in Sources */, 19A17E3F47DA087E2B76D087 /* QueryTests.swift in Sources */, @@ -1375,6 +1397,7 @@ 19A17F7977364EC8CD33C3C3 /* SelectTests.swift in Sources */, 19A17FD22EF43DF428DD93BA /* ValueTests.swift in Sources */, 19A177AA5922527BBDC77CF9 /* QueryIntegrationTests.swift in Sources */, + 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */, 19A179786A6826D58A70F8BC /* AggregateFunctionsTests.swift in Sources */, 19A1793972BDDDB027C113BB /* CustomAggregationTests.swift in Sources */, 19A1773155AC2BF2CA86A473 /* SetterTests.swift in Sources */, @@ -1401,6 +1424,7 @@ 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */, EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */, + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */, EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */, @@ -1439,6 +1463,7 @@ 19A1776BD5127DFDF847FF1F /* FTS5Tests.swift in Sources */, 19A173088B85A7E18E8582A7 /* FTSIntegrationTests.swift in Sources */, 19A178767223229E61C5066F /* FTS4Tests.swift in Sources */, + DBB93D5B2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, 19A1781CBA8968ABD3E00877 /* ExpressionTests.swift in Sources */, 19A17923494236793893BF72 /* StatementTests.swift in Sources */, 19A17A52BF29D27C9AA229E7 /* QueryTests.swift in Sources */, @@ -1455,6 +1480,7 @@ 19A17DE1FCDB5695702AD24D /* SelectTests.swift in Sources */, 19A1726002D24C14F876C8FE /* ValueTests.swift in Sources */, 19A173389E53CB24DFA8CEDD /* QueryIntegrationTests.swift in Sources */, + 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */, 19A170C56745F9D722A73D77 /* AggregateFunctionsTests.swift in Sources */, 19A1772EBE65173EDFB1AFCA /* CustomAggregationTests.swift in Sources */, 19A17E0ABA6C415F014CD51C /* SetterTests.swift in Sources */, diff --git a/Sources/SQLite/Core/Value.swift b/Sources/SQLite/Core/Value.swift index 9c463f0c..249a7728 100644 --- a/Sources/SQLite/Core/Value.swift +++ b/Sources/SQLite/Core/Value.swift @@ -39,7 +39,7 @@ public protocol Value: Expressible { // extensions cannot have inheritance claus static var declaredDatatype: String { get } - static func fromDatatypeValue(_ datatypeValue: Datatype) -> ValueType + static func fromDatatypeValue(_ datatypeValue: Datatype) throws -> ValueType var datatypeValue: Datatype { get } diff --git a/Sources/SQLite/Helpers.swift b/Sources/SQLite/Helpers.swift index adfadc8a..c27ccf05 100644 --- a/Sources/SQLite/Helpers.swift +++ b/Sources/SQLite/Helpers.swift @@ -121,9 +121,9 @@ func transcode(_ literal: Binding?) -> String { } } -// swiftlint:disable force_cast +// swiftlint:disable force_cast force_try func value(_ binding: Binding) -> A { - A.fromDatatypeValue(binding as! A.Datatype) as! A + try! A.fromDatatypeValue(binding as! A.Datatype) as! A } func value(_ binding: Binding?) -> A { diff --git a/Sources/SQLite/Schema/SchemaDefinitions.swift b/Sources/SQLite/Schema/SchemaDefinitions.swift index 2d38e1fb..80f9e199 100644 --- a/Sources/SQLite/Schema/SchemaDefinitions.swift +++ b/Sources/SQLite/Schema/SchemaDefinitions.swift @@ -57,7 +57,19 @@ public struct ColumnDefinition: Equatable { } init(_ string: String) { - self = Affinity.allCases.first { $0.rawValue.lowercased() == string.lowercased() } ?? .TEXT + let test = string.uppercased() + // https://sqlite.org/datatype3.html#determination_of_column_affinity + if test.contains("INT") { // Rule 1 + self = .INTEGER + } else if ["CHAR", "CLOB", "TEXT"].first(where: {test.contains($0)}) != nil { // Rule 2 + self = .TEXT + } else if string.contains("BLOB") { // Rule 3 + self = .BLOB + } else if ["REAL", "FLOA", "DOUB"].first(where: {test.contains($0)}) != nil { // Rule 4 + self = .REAL + } else { // Rule 5 + self = .NUMERIC + } } } @@ -107,7 +119,7 @@ public struct ColumnDefinition: Equatable { public struct ForeignKey: Equatable { let table: String let column: String - let primaryKey: String + let primaryKey: String? let onUpdate: String? let onDelete: String? } @@ -365,7 +377,7 @@ extension ColumnDefinition.ForeignKey { ([ "REFERENCES", table.quote(), - "(\(primaryKey.quote()))", + primaryKey.map { "(\($0.quote()))" }, onUpdate.map { "ON UPDATE \($0)" }, onDelete.map { "ON DELETE \($0)" } ] as [String?]).compactMap { $0 } diff --git a/Sources/SQLite/Schema/SchemaReader.swift b/Sources/SQLite/Schema/SchemaReader.swift index 2be15f29..1989ad6f 100644 --- a/Sources/SQLite/Schema/SchemaReader.swift +++ b/Sources/SQLite/Schema/SchemaReader.swift @@ -187,7 +187,7 @@ private enum ForeignKeyListTable { static let seqColumn = Expression("seq") static let tableColumn = Expression("table") static let fromColumn = Expression("from") - static let toColumn = Expression("to") + static let toColumn = Expression("to") // when null, use primary key static let onUpdateColumn = Expression("on_update") static let onDeleteColumn = Expression("on_delete") static let matchColumn = Expression("match") diff --git a/Sources/SQLite/Typed/AggregateFunctions.swift b/Sources/SQLite/Typed/AggregateFunctions.swift index bf4fb8fc..17fc4a20 100644 --- a/Sources/SQLite/Typed/AggregateFunctions.swift +++ b/Sources/SQLite/Typed/AggregateFunctions.swift @@ -166,7 +166,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// salary.average /// // avg("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `avg` aggregate /// function. public var average: Expression { Function.avg.wrap(self) @@ -179,7 +179,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// salary.sum /// // sum("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `sum` aggregate /// function. public var sum: Expression { Function.sum.wrap(self) @@ -192,7 +192,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// salary.total /// // total("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `total` aggregate /// function. public var total: Expression { Function.total.wrap(self) diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index c59b728f..2a83665c 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -1097,7 +1097,7 @@ extension Connection { public func scalar(_ query: ScalarQuery) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } public func scalar(_ query: Select) throws -> V { @@ -1108,7 +1108,7 @@ extension Connection { public func scalar(_ query: Select) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } public func pluck(_ query: QueryType) throws -> Row? { @@ -1200,9 +1200,9 @@ public struct Row { } public func get(_ column: Expression) throws -> V? { - func valueAtIndex(_ idx: Int) -> V? { + func valueAtIndex(_ idx: Int) throws -> V? { guard let value = values[idx] as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) as? V + return try V.fromDatatypeValue(value) as? V } guard let idx = columnNames[column.template] else { @@ -1224,10 +1224,10 @@ public struct Row { similar: columnNames.keys.filter(similar).sorted() ) } - return valueAtIndex(columnNames[firstIndex].value) + return try valueAtIndex(columnNames[firstIndex].value) } - return valueAtIndex(idx) + return try valueAtIndex(idx) } public subscript(column: Expression) -> T { diff --git a/Sources/SQLite/Typed/WindowFunctions.swift b/Sources/SQLite/Typed/WindowFunctions.swift new file mode 100644 index 00000000..e71e1ada --- /dev/null +++ b/Sources/SQLite/Typed/WindowFunctions.swift @@ -0,0 +1,145 @@ +import Foundation + +// see https://www.sqlite.org/windowfunctions.html#builtins +private enum WindowFunction: String { + // swiftlint:disable identifier_name + case ntile + case row_number + case rank + case dense_rank + case percent_rank + case cume_dist + case lag + case lead + case first_value + case last_value + case nth_value + // swiftlint:enable identifier_name + + func wrap(_ value: Int? = nil) -> Expression { + if let value { + return self.rawValue.wrap(Expression(value: value)) + } + return Expression(literal: "\(rawValue)()") + } + + func over(value: Int? = nil, _ orderBy: Expressible) -> Expression { + return Expression(" ".join([ + self.wrap(value), + Expression("OVER (ORDER BY \(orderBy.expression.template))", orderBy.expression.bindings) + ]).expression) + } + + func over(valueExpr: Expressible, _ orderBy: Expressible) -> Expression { + return Expression(" ".join([ + self.rawValue.wrap(valueExpr), + Expression("OVER (ORDER BY \(orderBy.expression.template))", orderBy.expression.bindings) + ]).expression) + } +} + +extension ExpressionType where UnderlyingType: Value { + /// Builds a copy of the expression with `lag(self, offset, default) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `lag(self, offset, default) OVER (ORDER BY {orderBy})` window function + public func lag(offset: Int = 0, default: Expressible? = nil, _ orderBy: Expressible) -> Expression { + if let defaultExpression = `default` { + return Expression( + "lag(\(template), \(offset), \(defaultExpression.asSQL())) OVER (ORDER BY \(orderBy.expression.template))", + bindings + orderBy.expression.bindings + ) + + } + return Expression("lag(\(template), \(offset)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } + + /// Builds a copy of the expression with `lead(self, offset, default) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `lead(self, offset, default) OVER (ORDER BY {orderBy})` window function + public func lead(offset: Int = 0, default: Expressible? = nil, _ orderBy: Expressible) -> Expression { + if let defaultExpression = `default` { + return Expression( + "lead(\(template), \(offset), \(defaultExpression.asSQL())) OVER (ORDER BY \(orderBy.expression.template))", + bindings + orderBy.expression.bindings) + + } + return Expression("lead(\(template), \(offset)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } + + /// Builds a copy of the expression with `first_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `first_value(self) OVER (ORDER BY {orderBy})` window function + public func firstValue(_ orderBy: Expressible) -> Expression { + WindowFunction.first_value.over(valueExpr: self, orderBy) + } + + /// Builds a copy of the expression with `last_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `last_value(self) OVER (ORDER BY {orderBy})` window function + public func lastValue(_ orderBy: Expressible) -> Expression { + WindowFunction.last_value.over(valueExpr: self, orderBy) + } + + /// Builds a copy of the expression with `nth_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter index: Row N of the window frame to return + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `nth_value(self) OVER (ORDER BY {orderBy})` window function + public func value(_ index: Int, _ orderBy: Expressible) -> Expression { + Expression("nth_value(\(template), \(index)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } +} + +/// Builds an expression representing `ntile(size) OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `ntile(size) OVER (ORDER BY {orderBy})` +public func ntile(_ size: Int, _ orderBy: Expressible) -> Expression { +// Expression.ntile(size, orderBy) + + WindowFunction.ntile.over(value: size, orderBy) +} + +/// Builds an expression representing `row_count() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `row_count() OVER (ORDER BY {orderBy})` +public func rowNumber(_ orderBy: Expressible) -> Expression { + WindowFunction.row_number.over(orderBy) +} + +/// Builds an expression representing `rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `rank() OVER (ORDER BY {orderBy})` +public func rank(_ orderBy: Expressible) -> Expression { + WindowFunction.rank.over(orderBy) +} + +/// Builds an expression representing `dense_rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `dense_rank() OVER ('over')` +public func denseRank(_ orderBy: Expressible) -> Expression { + WindowFunction.dense_rank.over(orderBy) +} + +/// Builds an expression representing `percent_rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `percent_rank() OVER (ORDER BY {orderBy})` +public func percentRank(_ orderBy: Expressible) -> Expression { + WindowFunction.percent_rank.over(orderBy) +} + +/// Builds an expression representing `cume_dist() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `cume_dist() OVER (ORDER BY {orderBy})` +public func cumeDist(_ orderBy: Expressible) -> Expression { + WindowFunction.cume_dist.over(orderBy) +} diff --git a/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift b/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift index ef97b981..8b7e27e3 100644 --- a/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift @@ -71,8 +71,68 @@ class AffinityTests: XCTestCase { XCTAssertEqual(ColumnDefinition.Affinity("NUMERIC"), .NUMERIC) } - func test_returns_TEXT_for_unknown_type() { - XCTAssertEqual(ColumnDefinition.Affinity("baz"), .TEXT) + // [Determination Of Column Affinity](https://sqlite.org/datatype3.html#determination_of_column_affinity) + // Rule 1 + func testIntegerAffinity() { + let declared = [ + "INT", + "INTEGER", + "TINYINT", + "SMALLINT", + "MEDIUMINT", + "BIGINT", + "UNSIGNED BIG INT", + "INT2", + "INT8" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .INTEGER})) + } + + // Rule 2 + func testTextAffinity() { + let declared = [ + "CHARACTER(20)", + "VARCHAR(255)", + "VARYING CHARACTER(255)", + "NCHAR(55)", + "NATIVE CHARACTER(70)", + "NVARCHAR(100)", + "TEXT", + "CLOB" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .TEXT})) + } + + // Rule 3 + func testBlobAffinity() { + XCTAssertEqual(ColumnDefinition.Affinity("BLOB"), .BLOB) + } + + // Rule 4 + func testRealAffinity() { + let declared = [ + "REAL", + "DOUBLE", + "DOUBLE PRECISION", + "FLOAT" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .REAL})) + } + + // Rule 5 + func testNumericAffinity() { + let declared = [ + "NUMERIC", + "DECIMAL(10,5)", + "BOOLEAN", + "DATE", + "DATETIME" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .NUMERIC})) + } + + func test_returns_NUMERIC_for_unknown_type() { + XCTAssertEqual(ColumnDefinition.Affinity("baz"), .NUMERIC) } } diff --git a/Tests/SQLiteTests/Schema/SchemaReaderTests.swift b/Tests/SQLiteTests/Schema/SchemaReaderTests.swift index 49871c10..d57ebff7 100644 --- a/Tests/SQLiteTests/Schema/SchemaReaderTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaReaderTests.swift @@ -40,7 +40,7 @@ class SchemaReaderTests: SQLiteTestCase { references: nil), ColumnDefinition(name: "admin", primaryKey: nil, - type: .TEXT, + type: .NUMERIC, nullable: false, defaultValue: .numericLiteral("0"), references: nil), @@ -51,7 +51,7 @@ class SchemaReaderTests: SQLiteTestCase { references: .init(table: "users", column: "manager_id", primaryKey: "id", onUpdate: nil, onDelete: nil)), ColumnDefinition(name: "created_at", primaryKey: nil, - type: .TEXT, + type: .NUMERIC, nullable: true, defaultValue: .NULL, references: nil) @@ -172,6 +172,44 @@ class SchemaReaderTests: SQLiteTestCase { ]) } + func test_foreignKeys_references_column() throws { + let sql = """ + CREATE TABLE artist( + artistid INTEGER PRIMARY KEY, + artistname TEXT + ); + CREATE TABLE track( + trackid INTEGER, + trackname TEXT, + trackartist INTEGER REFERENCES artist(artistid) + ); + """ + try db.execute(sql) + let trackColumns = try db.schema.foreignKeys(table: "track") + XCTAssertEqual(trackColumns.map { $0.toSQL() }.joined(separator: "\n"), """ + REFERENCES "artist" ("artistid") + """) + } + + func test_foreignKeys_references_null_column() throws { + let sql = """ + CREATE TABLE artist( + artistid INTEGER PRIMARY KEY, + artistname TEXT + ); + CREATE TABLE track( + trackid INTEGER, + trackname TEXT, + trackartist INTEGER REFERENCES artist + ); + """ + try db.execute(sql) + let trackColumns = try db.schema.foreignKeys(table: "track") + XCTAssertEqual(trackColumns.map { $0.toSQL() }.joined(separator: "\n"), """ + REFERENCES "artist" + """) + } + func test_tableDefinitions() throws { let tables = try schemaReader.tableDefinitions() XCTAssertEqual(tables.count, 1) diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index d8d31a79..aa45cafe 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -322,6 +322,101 @@ class QueryIntegrationTests: SQLiteTestCase { XCTAssertNotNil(row[name]) XCTAssertNotNil(row[email]) } + + func test_select_ntile_function() throws { + let users = Table("users") + + try insertUser("Joey") + try insertUser("Timmy") + try insertUser("Jimmy") + try insertUser("Billy") + + let bucket = ntile(1, id.asc) + try db.prepare(users.select(id, bucket)).forEach { + XCTAssertEqual($0[bucket], 1) // only 1 window + } + } + + func test_select_cume_dist_function() throws { + let users = Table("users") + + try insertUser("Joey") + try insertUser("Timmy") + try insertUser("Jimmy") + try insertUser("Billy") + + let cumeDist = cumeDist(email) + let results = try db.prepare(users.select(id, cumeDist)).map { + $0[cumeDist] + } + XCTAssertEqual([0.25, 0.5, 0.75, 1], results) + } + + func test_select_window_row_number() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let rowNumber = rowNumber(email.asc) + var expectedRowNum = 1 + try db.prepare(users.select(id, rowNumber)).forEach { + // should retrieve row numbers in order of INSERT above + XCTAssertEqual($0[rowNumber], expectedRowNum) + expectedRowNum += 1 + } + } + + func test_select_window_ranking() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let percentRank = percentRank(email) + let actualPercentRank: [Int] = try db.prepare(users.select(id, percentRank)).map { + Int($0[percentRank] * 100) + } + XCTAssertEqual([0, 33, 66, 100], actualPercentRank) + + let rank = rank(email) + let actualRank: [Int] = try db.prepare(users.select(id, rank)).map { + $0[rank] + } + XCTAssertEqual([1, 2, 3, 4], actualRank) + + let denseRank = denseRank(email) + let actualDenseRank: [Int] = try db.prepare(users.select(id, denseRank)).map { + $0[denseRank] + } + XCTAssertEqual([1, 2, 3, 4], actualDenseRank) + } + + func test_select_window_values() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let firstValue = email.firstValue(email.desc) + try db.prepare(users.select(id, firstValue)).forEach { + XCTAssertEqual($0[firstValue], "Timmy@example.com") // should grab last email alphabetically + } + + let lastValue = email.lastValue(email.asc) + var row = try db.pluck(users.select(id, lastValue))! + XCTAssertEqual(row[lastValue], "Billy@example.com") + + let nthValue = email.value(1, email.asc) + row = try db.pluck(users.select(id, nthValue))! + XCTAssertEqual(row[nthValue], "Billy@example.com") + } } extension Connection { diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift index 506f6b10..14fa373b 100644 --- a/Tests/SQLiteTests/Typed/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -85,4 +85,34 @@ class RowTests: XCTestCase { } } } + + public func test_get_datatype_throws() { + // swiftlint:disable nesting + struct MyType: Value { + enum MyError: Error { + case failed + } + + public static var declaredDatatype: String { + Blob.declaredDatatype + } + + public static func fromDatatypeValue(_ dataValue: Blob) throws -> Data { + throw MyError.failed + } + + public var datatypeValue: Blob { + return Blob(bytes: []) + } + } + + let row = Row(["\"foo\"": 0], [Blob(bytes: [])]) + XCTAssertThrowsError(try row.get(Expression("foo"))) { error in + if case MyType.MyError.failed = error { + XCTAssertTrue(true) + } else { + XCTFail("unexpected error: \(error)") + } + } + } } diff --git a/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift new file mode 100644 index 00000000..6ded152b --- /dev/null +++ b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift @@ -0,0 +1,58 @@ +import XCTest +import SQLite + +class WindowFunctionsTests: XCTestCase { + + func test_ntile_wrapsExpressionWithOverClause() { + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + assertSQL("ntile(20) OVER (ORDER BY \"intOptional\" ASC)", ntile(20, intOptional.asc)) + assertSQL("ntile(20) OVER (ORDER BY \"double\" ASC)", ntile(20, double.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"doubleOptional\" ASC)", ntile(1, doubleOptional.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + } + + func test_row_number_wrapsExpressionWithOverClause() { + assertSQL("row_number() OVER (ORDER BY \"int\" DESC)", rowNumber(int.desc)) + } + + func test_rank_wrapsExpressionWithOverClause() { + assertSQL("rank() OVER (ORDER BY \"int\" DESC)", rank(int.desc)) + } + + func test_dense_rank_wrapsExpressionWithOverClause() { + assertSQL("dense_rank() OVER (ORDER BY \"int\" DESC)", denseRank(int.desc)) + } + + func test_percent_rank_wrapsExpressionWithOverClause() { + assertSQL("percent_rank() OVER (ORDER BY \"int\" DESC)", percentRank(int.desc)) + } + + func test_cume_dist_wrapsExpressionWithOverClause() { + assertSQL("cume_dist() OVER (ORDER BY \"int\" DESC)", cumeDist(int.desc)) + } + + func test_lag_wrapsExpressionWithOverClause() { + assertSQL("lag(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lag(int.desc)) + assertSQL("lag(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 7, int.desc)) + assertSQL("lag(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 1, default: Expression(value: 3), int.desc)) + } + + func test_lead_wrapsExpressionWithOverClause() { + assertSQL("lead(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lead(int.desc)) + assertSQL("lead(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 7, int.desc)) + assertSQL("lead(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 1, default: Expression(value: 3), int.desc)) + } + + func test_firstValue_wrapsExpressionWithOverClause() { + assertSQL("first_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.firstValue(int.desc)) + assertSQL("first_value(\"double\") OVER (ORDER BY \"int\" DESC)", double.firstValue(int.desc)) + } + + func test_lastValue_wrapsExpressionWithOverClause() { + assertSQL("last_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.lastValue(int.desc)) + } + + func test_nth_value_wrapsExpressionWithOverClause() { + assertSQL("nth_value(\"int\", 3) OVER (ORDER BY \"int\" DESC)", int.value(3, int.desc)) + } +}