diff --git a/Sources/ClickHouseVapor/Application+ClickHouseNIO.swift b/Sources/ClickHouseVapor/Application+ClickHouseNIO.swift index b6805d7..83d7b5e 100644 --- a/Sources/ClickHouseVapor/Application+ClickHouseNIO.swift +++ b/Sources/ClickHouseVapor/Application+ClickHouseNIO.swift @@ -5,8 +5,8 @@ // Created by Patrick Zippenfenig on 2020-11-10. // -import Vapor import ClickHouseNIO +import Vapor @_exported import struct NIO.TimeAmount @@ -34,13 +34,14 @@ public struct ClickHousePoolConfiguration { public let maxConnectionsPerEventLoop: Int public let requestTimeout: TimeAmount - public init(hostname: String = "localhost", - port: Int = ClickHouseConnection.defaultPort, - user: String? = nil, - password: String? = nil, - database: String? = nil, - maxConnectionsPerEventLoop: Int = 1, - requestTimeout: TimeAmount = .seconds(10) + public init( + hostname: String = "localhost", + port: Int = ClickHouseConnection.defaultPort, + user: String? = nil, + password: String? = nil, + database: String? = nil, + maxConnectionsPerEventLoop: Int = 1, + requestTimeout: TimeAmount = .seconds(10) ) throws { self.configuration = try ClickHouseConfiguration( hostname: hostname, diff --git a/Sources/ClickHouseVapor/ClickHouseColumnConvertible.swift b/Sources/ClickHouseVapor/ClickHouseColumnConvertible.swift index 62c0b2e..acfd4ae 100644 --- a/Sources/ClickHouseVapor/ClickHouseColumnConvertible.swift +++ b/Sources/ClickHouseVapor/ClickHouseColumnConvertible.swift @@ -7,8 +7,6 @@ import ClickHouseNIO - - /// Define how a colun can be converted into a clickhose datatype public protocol ClickHouseColumnConvertible: AnyObject { var key: String { get } @@ -33,7 +31,6 @@ public protocol ClickHouseColumnConvertibleTyped: ClickHouseColumnConvertible { associatedtype Value: ClickHouseDataType var wrappedValue: [Value] { get set } var columnMetadata: ClickHouseColumnMetadata? { get } - } extension ClickHouseColumnConvertibleTyped { @@ -89,7 +86,6 @@ public final class Field: ClickHouseColumnConvertible self } - fileprivate init( key: String, isPrimary: Bool = false, @@ -209,7 +205,7 @@ extension Array { /// Only include column rows where the isIncluded array is true func filtered(_ isIncluded: [Bool]) -> Self { precondition(count == isIncluded.count) - var arr = Self.init() + var arr = Self() let count = isIncluded.reduce(0, { $0 + ($1 ? 1 : 0) }) arr.reserveCapacity(count) for (i, include) in isIncluded.enumerated() where include { diff --git a/Sources/ClickHouseVapor/ClickHouseEngine.swift b/Sources/ClickHouseVapor/ClickHouseEngine.swift index 5b16c13..8484489 100644 --- a/Sources/ClickHouseVapor/ClickHouseEngine.swift +++ b/Sources/ClickHouseVapor/ClickHouseEngine.swift @@ -8,9 +8,9 @@ import Foundation import enum ClickHouseNIO.ClickHouseTypeName -/// Abstract a clickhouse storage engine. For example the `ReplacingMergeTree` requires special CREATE syntax. +/// Abstract a clickHouse storage engine. For example the `ReplacingMergeTree` requires special CREATE syntax. /// This could also be used to -public protocol ClickHouseEngine { +public protocol ClickHouseEngine: Sendable { /// Generate a SQL query to create the table using the defined model columns func createTableQuery(columns: [ClickHouseColumnConvertible]) -> String @@ -26,23 +26,23 @@ public protocol ClickHouseEngine { extension ClickHouseEngine { public var isUsingCluster: Bool { - cluster != nil + self.cluster != nil } - /// Returns the tablename and database name encoded with a dot + /// Returns the table name and database name encoded with a dot public var tableWithDatabase: String { - if let database = database { - return "`\(database)`.`\(table)`" + if let database = self.database { + return "`\(database)`.`\(self.table)`" } - return "`\(table)`" + return "`\(self.table)`" } } public struct ClickHouseEngineReplacingMergeTree: ClickHouseEngine { - public var table: String - public var database: String? - public var cluster: String? - public var partitionBy: String? + public let table: String + public let database: String? + public let cluster: String? + public let partitionBy: String? public init(table: String, database: String?, cluster: String?, partitionBy: String?) { self.table = table @@ -77,7 +77,7 @@ public struct ClickHouseEngineReplacingMergeTree: ClickHouseEngine { PRIMARY KEY (\(ids.joined(separator: ","))) """ if let partitionBy = partitionBy { - query += " PARTITION BY (\(partitionBy))" + query += " PARTITION BY (\(partitionBy))" } query += " ORDER BY (\(order.joined(separator: ",")))" return query @@ -89,50 +89,16 @@ extension ClickHouseTypeName { // basically all numerical data types except for Decimal support LowCardinality // https://clickhouse.tech/docs/en/sql-reference/data-types/lowcardinality/ switch self { - case .float: - return true - case .float64: - return true - case .int8: - return true - case .int16: - return true - case .int32: - return true - case .int64: - return true - case .uint8: - return true - case .uint16: - return true - case .uint32: - return true - case .uint64: + case .float, .float64, .int8, .int16, .int32, .int64, .uint8, .uint16, .uint32, .uint64: return true case .uuid: return false - case .fixedString(_): - return true - case .string: + case .fixedString, .string: return true case .nullable(let type): return type.supportsLowCardinality - case .array: + case .array, .boolean, .date, .date32, .dateTime, .dateTime64, .enum16, .enum8: return false - case .boolean: - return false - case .date: - return false - case .date32: - return false - case .dateTime(_): - return false - case .dateTime64(_): - return false - case .enum16(_): - return false - case .enum8(_): - return false -} + } } } diff --git a/Sources/ClickHouseVapor/ClickHouseModel.swift b/Sources/ClickHouseVapor/ClickHouseModel.swift index 290137f..7b97840 100644 --- a/Sources/ClickHouseVapor/ClickHouseModel.swift +++ b/Sources/ClickHouseVapor/ClickHouseModel.swift @@ -5,10 +5,9 @@ // Created by Patrick Zippenfenig on 2020-11-10. // +import ClickHouseNIO import Foundation import Vapor -import ClickHouseNIO - public protocol ClickHouseModel: AnyObject { static var engine: ClickHouseEngine { get } @@ -19,7 +18,7 @@ extension ClickHouseModel { /// Get the number of rows. /// In case only some rows are populated (e.g. only certain ), the first column with more than 0 rows is considered. public var count: Int { - return properties.first(where: {$0.count > 0})?.count ?? 0 + return properties.first(where: { $0.count > 0 })?.count ?? 0 } /// Only include column rows where the isIncluded array is true. @@ -55,7 +54,7 @@ extension ClickHouseModel { allMirrors.append(superMirror) } return allMirrors.reversed().flatMap { m in - m.children.compactMap { + m.children.compactMap { $0.value as? ClickHouseColumnConvertible } } @@ -66,7 +65,7 @@ extension ClickHouseModel { on connection: ClickHouseConnectionProtocol, engine: ClickHouseEngine? = nil ) -> EventLoopFuture { - let fields = Self.init().properties + let fields = Self().properties let engine = engine ?? Self.engine let query = engine.createTableQuery(columns: fields) connection.logger.debug("\(query)") @@ -128,11 +127,11 @@ extension ClickHouseModel { sql: String ) -> EventLoopFuture { connection.logger.debug("\(sql)") - let this = Self.init() + let this = Self() let properties = this.properties return connection.query(sql: sql).flatMapThrowing { res -> Self in try res.columns.forEach { column in - guard let prop = properties.first(where: {$0.key == column.name}) else { + guard let prop = properties.first(where: { $0.key == column.name }) else { return } try prop.setClickHouseArray(column.values) @@ -153,9 +152,8 @@ extension ClickHouseModel { offset: Int? = nil, engine: ClickHouseEngine? = nil ) -> EventLoopFuture { - let engine = engine ?? Self.engine - let fields = fields ?? Self.init().properties.map { "`\($0.key)`" } + let fields = fields ?? Self().properties.map { "`\($0.key)`" } var sql = "SELECT " sql += fields.joined(separator: ",") diff --git a/Tests/ClickHouseVaporTests/ClickHouseVaporTests.swift b/Tests/ClickHouseVaporTests/ClickHouseVaporTests.swift index 66509f4..232d257 100644 --- a/Tests/ClickHouseVaporTests/ClickHouseVaporTests.swift +++ b/Tests/ClickHouseVaporTests/ClickHouseVaporTests.swift @@ -1,7 +1,8 @@ -import XCTest -@testable import ClickHouseVapor import Foundation import Vapor +import XCTest + +@testable import ClickHouseVapor extension Application { func configureClickHouseDatabases() throws { @@ -40,10 +41,9 @@ public class TestModel: ClickHouseModel { @Field(key: "dat") var dat: [ClickHouseDate] - /// Not implemented on test-server // @Field(key: "dat32") - // var dat32: [ClickHouseDate32] + // var dat32: [ClickHouseDate32] @Field(key: "datt") var datt: [ ClickHouseDateTime ] @@ -53,22 +53,20 @@ public class TestModel: ClickHouseModel { @Field(key: "datt64", precision: 3) var datt64: [ ClickHouseDateTime64 ] - + @Field(key: "datt64z", precision: 3, timeZone: "'GMT'") var datt64z: [ ClickHouseDateTime64 ] - + @Field(key: "en8", mapping: ["a": 0, "b": 1]) var en8: [ ClickHouseEnum8 ] - + @Field(key: "en16", mapping: ["a": 12, "b": 1, "c": 600]) var en16: [ ClickHouseEnum16 ] @Field(key: "temperature") var temperature: [Float] - required public init() { - - } + public required init() {} public static var engine: ClickHouseEngine { return ClickHouseEngineReplacingMergeTree( @@ -105,7 +103,6 @@ public final class InheritedTestModel: TestParentClass, ClickHouseModel { } final class ClickHouseVaporTests: XCTestCase { - static var allTests = [ ("testPing", testPing), ("testModel", testModel), @@ -134,7 +131,7 @@ final class ClickHouseVaporTests: XCTestCase { model.id = [ "x010", "ax51", "cd22" ] model.fixed = [ "", "123456", "12345678901234" ] - model.arr = [[1], [], [76, 56, 2]] + model.arr = [[1], [], [76, 56, 2]] model.dat = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault] model.datt = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault] model.datt64 = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault] @@ -144,7 +141,7 @@ final class ClickHouseVaporTests: XCTestCase { model.en16 = [.init(word: "a"), .init(word: "b"), .init(word: "c")] model.timestamp = [ 100, 200, 300 ] model.temperature = [ 11.1, 10.4, 8.9 ] - + let createQuery = TestModel.engine.createTableQuery(columns: model.properties) XCTAssertEqual(createQuery .replacingOccurrences(of: "Enum8('b'=1,'a'=0)", with: "Enum8('a'=0,'b'=1)") @@ -166,16 +163,16 @@ final class ClickHouseVaporTests: XCTestCase { XCTAssertEqual(model.id, model2.id) XCTAssertEqual(["", "123456", "1234567890"], model2.fixed) XCTAssertEqual(model.timestamp, model2.timestamp) - XCTAssertEqual(model.dat.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model2.dat.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model.datt.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model2.datt.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model.dattz.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model2.dattz.map { $0.date}, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) - XCTAssertEqual(model.en8.map { $0.word}, ["a", "b", "a"]) - XCTAssertEqual(model2.en8.map { $0.word}, ["a", "b", "a"]) - XCTAssertEqual(model.en16.map { $0.word}, ["a", "b", "c"]) - XCTAssertEqual(model2.en16.map { $0.word}, ["a", "b", "c"]) + XCTAssertEqual(model.dat.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model2.dat.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model.datt.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model2.datt.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model.dattz.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model2.dattz.map { $0.date }, [Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0), Date(timeIntervalSince1970: 0.0)]) + XCTAssertEqual(model.en8.map { $0.word }, ["a", "b", "a"]) + XCTAssertEqual(model2.en8.map { $0.word }, ["a", "b", "a"]) + XCTAssertEqual(model.en16.map { $0.word }, ["a", "b", "c"]) + XCTAssertEqual(model2.en16.map { $0.word }, ["a", "b", "c"]) XCTAssertEqual(model.arr, [[1], [], [76, 56, 2]]) XCTAssertEqual(model2.arr, [[1], [], [76, 56, 2]]) @@ -192,7 +189,7 @@ final class ClickHouseVaporTests: XCTestCase { XCTAssertEqual(filtered.id, ["ax51", "x010"]) XCTAssertEqual(filtered.timestamp, [200, 100]) - /// Raw select query, that gets applied to + // Raw select query, that gets applied to let model3 = try! TestModel.select( on: app.clickHouse, sql: "SELECT timestamp, stationID FROM default.test" @@ -210,7 +207,8 @@ final class ClickHouseVaporTests: XCTestCase { let model = InheritedTestModel() let createQuery = InheritedTestModel.engine.createTableQuery(columns: model.properties) - XCTAssertEqual(createQuery, + XCTAssertEqual( + createQuery, """ CREATE TABLE IF NOT EXISTS `testInherited` (timestamp Int64,stationID LowCardinality(String),temperature Float32) ENGINE = ReplacingMergeTree() @@ -222,7 +220,7 @@ final class ClickHouseVaporTests: XCTestCase { try! InheritedTestModel.deleteTable(on: app.clickHouse).wait() // create table try! InheritedTestModel.createTable(on: app.clickHouse).wait() - + // fill model with data and insert it model.id = [ "x010", "ax51", "cd22" ] model.timestamp = [ 100, 200, 300 ] @@ -232,9 +230,9 @@ final class ClickHouseVaporTests: XCTestCase { // select the data again let model2 = try! InheritedTestModel.select(on: app.clickHouse).wait() - XCTAssertEqual(model2.id, model.id) - XCTAssertEqual(model2.timestamp, model.timestamp) - XCTAssertEqual(model2.temperature, model.temperature) + XCTAssertEqual(model2.id, model.id) + XCTAssertEqual(model2.timestamp, model.timestamp) + XCTAssertEqual(model2.temperature, model.temperature) } /// insert should fail if some columns are not set, but others are