Skip to content

Commit

Permalink
Add support for new types added in ClickHouseNIO 1.4 (#4)
Browse files Browse the repository at this point in the history
* added support for new types, updated tests

* added tests for arrays, updated array cardinality

* fixed bool cardinality (Bool and Dates have to be  enabled on the server)

* updated dependency, removed fixed todo

Co-authored-by: Roland Ambs <[email protected]>
  • Loading branch information
planetMDX and Roland Ambs authored Jan 17, 2023
1 parent 5e5b14f commit cc3b0b1
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/patrick-zippenfenig/ClickHouseNIO.git", from: "1.3.0")
.package(url: "https://github.com/patrick-zippenfenig/ClickHouseNIO.git", from: "1.4.1")
],
targets: [
.target(
Expand Down
7 changes: 7 additions & 0 deletions Sources/ClickHouseVapor/Application+ClickHouseNIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import ClickHouseNIO

@_exported import struct NIO.TimeAmount

@_exported import struct ClickHouseNIO.ClickHouseDate
@_exported import struct ClickHouseNIO.ClickHouseDate32
@_exported import struct ClickHouseNIO.ClickHouseDateTime
@_exported import struct ClickHouseNIO.ClickHouseDateTime64
@_exported import struct ClickHouseNIO.ClickHouseEnum8
@_exported import struct ClickHouseNIO.ClickHouseEnum16

/// Vapor `Application.ClickHouse` and `Request.ClickHouse` implement this procotol to be used later for queries
public protocol ClickHouseConnectionProtocol {
var eventLoop: EventLoop { get }
Expand Down
111 changes: 105 additions & 6 deletions Sources/ClickHouseVapor/ClickHouseColumnConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import ClickHouseNIO



/// Define how a colun can be converted into a clickhose datatype
public protocol ClickHouseColumnConvertible: AnyObject {
var key: String { get }
Expand All @@ -30,7 +32,7 @@ public protocol ClickHouseColumnConvertible: AnyObject {
public protocol ClickHouseColumnConvertibleTyped: ClickHouseColumnConvertible {
associatedtype Value: ClickHouseDataType
var wrappedValue: [Value] { get set }
var fixedStringLen: Int? { get }
var columnMetadata: ClickHouseColumnMetadata? { get }

}

Expand Down Expand Up @@ -70,7 +72,7 @@ extension ClickHouseColumnConvertibleTyped {
}

public func clickHouseTypeName() -> ClickHouseTypeName {
return Value.getClickHouseTypeName(fixedLength: fixedStringLen)
return Value.getClickHouseTypeName(columnMetadata: columnMetadata)
}
}

Expand All @@ -81,26 +83,123 @@ public final class Field<Value: ClickHouseDataType>: ClickHouseColumnConvertible
public let isPrimary: Bool
public let isOrderBy: Bool
public let isLowCardinality: Bool
public let fixedStringLen: Int?
public let columnMetadata: ClickHouseColumnMetadata?

public var projectedValue: Field<Value> {
self
}

public init(

fileprivate init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
fixedStringLen: Int? = nil
columnMetadata: ClickHouseColumnMetadata
) {
self.key = key
self.isPrimary = isPrimary
self.isOrderBy = isOrderBy
self.isLowCardinality = isLowCardinality
self.fixedStringLen = fixedStringLen
self.columnMetadata = columnMetadata
self.wrappedValue = []
}
public init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false
) {
self.key = key
self.isPrimary = isPrimary
self.isOrderBy = isOrderBy
self.isLowCardinality = isLowCardinality
self.columnMetadata = nil
self.wrappedValue = []
}
}

extension Field where Value == String {
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
fixedStringLen: Int
) {
self.init(key: key, isPrimary: isPrimary, isOrderBy: isOrderBy, isLowCardinality: isLowCardinality, columnMetadata: .fixedStringLength(fixedStringLen))
}
}
extension Field where Value == ClickHouseDateTime {
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
timeZone: String? = nil
) {
self.init(key: key, isPrimary: isPrimary, isOrderBy: isOrderBy, isLowCardinality: isLowCardinality, columnMetadata: .dateTimeTimeZone(timeZone))
}

}
extension Field where Value == ClickHouseDateTime64 {
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
precision: Int,
timeZone: String? = nil
) {
self.init(key: key, isPrimary: isPrimary, isOrderBy: isOrderBy, isLowCardinality: isLowCardinality, columnMetadata: .dateTime64Precision(precision, timeZone))
}
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false
) {
fatalError("missing precision for DateTime64")
}
}
extension Field where Value == ClickHouseEnum8 {
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
mapping: [String: Int8]
) {
self.init(key: key, isPrimary: isPrimary, isOrderBy: isOrderBy, isLowCardinality: isLowCardinality, columnMetadata: .enum8Map(mapping))
}

convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false
) {
fatalError("missing enum-mapping for enum8")
}
}
extension Field where Value == ClickHouseEnum16 {
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false,
mapping: [String: Int16]
) {
self.init(key: key, isPrimary: isPrimary, isOrderBy: isOrderBy, isLowCardinality: isLowCardinality, columnMetadata: .enum16Map(mapping))
}
convenience init(
key: String,
isPrimary: Bool = false,
isOrderBy: Bool = false,
isLowCardinality: Bool = false
) {
fatalError("missing enum-mapping for enum16")
}
}

extension Array {
Expand Down
18 changes: 17 additions & 1 deletion Sources/ClickHouseVapor/ClickHouseEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ extension ClickHouseTypeName {
return true
case .nullable(let type):
return type.supportsLowCardinality
}
case .array(let type):
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
}
}
}
1 change: 1 addition & 0 deletions Sources/ClickHouseVapor/ClickHouseModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation
import Vapor
import ClickHouseNIO


public protocol ClickHouseModel: AnyObject {
static var engine: ClickHouseEngine { get }
init()
Expand Down
67 changes: 63 additions & 4 deletions Tests/ClickHouseVaporTests/ClickHouseVaporTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import XCTest
@testable import ClickHouseVapor
import Foundation
import Vapor

extension Application {
Expand Down Expand Up @@ -29,6 +30,39 @@ public class TestModel: ClickHouseModel {
@Field(key: "fixed", isLowCardinality: true, fixedStringLen: 10)
var fixed: [ String ]

@Field(key: "arr")
var arr: [ [Int64] ]

/// Not implemented on test-server
// @Field(key: "bol")
// var bol: [ Bool ]

@Field(key: "dat")
var dat: [ClickHouseDate]


/// Not implemented on test-server
// @Field(key: "dat32")
// var dat32: [ClickHouseDate32]

@Field(key: "datt")
var datt: [ ClickHouseDateTime ]

@Field(key: "dattz", timeZone: "'GMT'")
var dattz: [ ClickHouseDateTime ]

@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]

Expand Down Expand Up @@ -76,25 +110,50 @@ final class ClickHouseVaporTests: XCTestCase {

model.id = [ "x010", "ax51", "cd22" ]
model.fixed = [ "", "123456", "12345678901234" ]
model.arr = [[1], [], [76, 56, 2]]
model.dat = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault]
model.datt = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault]
model.datt64 = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault]
model.datt64z = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault]
model.dattz = [.clickhouseDefault, .clickhouseDefault, .clickhouseDefault]
model.en8 = [.init(word: "a"), .init(word: "b"), .init(word: "a")]
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, """
CREATE TABLE IF NOT EXISTS `test` (timestamp Int64,stationID LowCardinality(String),fixed LowCardinality(FixedString(10)),temperature Float32)
XCTAssertEqual(createQuery
.replacingOccurrences(of: "Enum8('b'=1,'a'=0)", with: "Enum8('a'=0,'b'=1)")
.replacingOccurrences(of: "Enum16('b'=1,'a'=12,'c'=600)", with: "Enum16('a'=12,'b'=1,'c'=600)")
.replacingOccurrences(of: "Enum16('b'=1,'c'=600,'a'=12)", with: "Enum16('a'=12,'b'=1,'c'=600)")
.replacingOccurrences(of: "Enum16('a'=12,'c'=600,'b'=1)", with: "Enum16('a'=12,'b'=1,'c'=600)")
.replacingOccurrences(of: "Enum16('c'=600,'b'=1,'a'=12)", with: "Enum16('a'=12,'b'=1,'c'=600)")
.replacingOccurrences(of: "Enum16('c'=600,'a'=12,'b'=1)", with: "Enum16('a'=12,'b'=1,'c'=600)"),
"""
CREATE TABLE IF NOT EXISTS `test` (timestamp Int64,stationID LowCardinality(String),fixed LowCardinality(FixedString(10)),arr Array(Int64),dat Date,datt DateTime,dattz DateTime('GMT'),datt64 DateTime64(3),datt64z DateTime64(3, 'GMT'),en8 Enum8('a'=0,'b'=1),en16 Enum16('a'=12,'b'=1,'c'=600),temperature Float32)
ENGINE = ReplacingMergeTree()
PRIMARY KEY (timestamp,stationID) PARTITION BY (toYYYYMM(toDateTime(timestamp))) ORDER BY (timestamp,stationID)
""")

try! TestModel.createTable(on: app.clickHouse).wait()
try! model.insert(on: app.clickHouse).wait()

let model2 = try! TestModel.select(on: app.clickHouse).wait()

XCTAssertEqual(model.temperature, model2.temperature)
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.arr, [[1], [], [76, 56, 2]])
XCTAssertEqual(model2.arr, [[1], [], [76, 56, 2]])

let filtered = try! TestModel.select(
on: app.clickHouse,
Expand Down

0 comments on commit cc3b0b1

Please sign in to comment.