├── .gitignore ├── README.md ├── db_clickhouse.nimble ├── src └── db_clickhouse.nim └── tests ├── test_clickhouse.nim └── test_clickhouse.nims /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | tests/test_clickhouse 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nim Clickhouse Interface 2 | 3 | ## Introduction 4 | 5 | This package let's you use the [Clickhouse](https://clickhouse.yandex/) 6 | analytical database from the [Nim](https://nim-lang.org) language, using an 7 | interface similar to the ones provided for SQLite and for PostgreSQL. 8 | 9 | It contains only pure Nim code and doesn't require any external library. 10 | 11 | Internally, it uses the HTTP interface of Clickhouse, since the TCP transport 12 | is not meant to be used by client applications but only for cross-engine 13 | communications, as you can see in 14 | [this bug report](https://github.com/yandex/ClickHouse/issues/45). 15 | 16 | ## Installation instructions 17 | 18 | You can find this package inside the [Nim Package Directory](https://nimble.directory/pkg/dbclickhouse). 19 | 20 | If you are using Nimble you can simply add the following line inside 21 | your dependencies: 22 | 23 | ```nim 24 | requires "dbclickhouse" 25 | ``` 26 | 27 | You can also set a version constraint on this package to select a certain 28 | release. 29 | 30 | ## Usage 31 | 32 | ```nim 33 | import db_clickhouse 34 | 35 | var db:DbConn = db_clickhouse.open("clickhouse-server") 36 | 37 | for row in db.getAllRows("SELECT * FROM test_table ORDER BY test_column"): 38 | echo row[0], row[1] 39 | 40 | db.close() 41 | ``` 42 | 43 | You can find other examples in the unit tests of this package. 44 | 45 | ## Tests 46 | 47 | If you want to run to unit tests included with this package, you will need to 48 | have an available ClickHouse instance with a test table with the following 49 | structure: 50 | 51 | ```sql 52 | CREATE TABLE test_table 53 | ( 54 | test_column String, 55 | test_column_two String 56 | ) ENGINE Memory; 57 | ``` 58 | 59 | If you are using Docker, you can create a new ClickHouse instance with the 60 | following command: 61 | 62 | ``` 63 | $ docker run -d \ 64 | --name clickhouse-server \ 65 | -p 9000:9000 \ 66 | -p 8123:8123 \ 67 | -v clickhouse:/var/lib/clickhouse \ 68 | yandex/clickhouse-server 69 | ``` 70 | 71 | You can create the required table starting a clickhouse client like this: 72 | 73 | ``` 74 | $ docker run \ 75 | -ti --rm \ 76 | --link clickhouse-server:clickhouse-server \ 77 | yandex/clickhouse-client \ 78 | --host clickhouse-server 79 | 80 | ClickHouse client version 1.1.54383. 81 | Connecting to clickhouse-server:9000. 82 | Connected to ClickHouse server version 1.1.54383. 83 | 84 | dec0c5819f76 :) CREATE TABLE test_table 85 | :-] ( 86 | :-] test_column String, 87 | :-] test_column_two String 88 | :-] ) ENGINE Memory; 89 | 90 | CREATE TABLE test_table 91 | ( 92 | test_column String, 93 | test_column_two String 94 | ) 95 | ENGINE = Memory 96 | 97 | Ok. 98 | 99 | 0 rows in set. Elapsed: 0.099 sec. 100 | 101 | dec0c5819f76 :) 102 | ``` 103 | 104 | You can then execute the unit tests using nimble: 105 | 106 | ``` 107 | $ nimble tests 108 | ``` 109 | 110 | If you want you can customize the host that will be used as ClickHouse server 111 | during the unit tests using the `TEST_DB_HOSTNAME` environment variable, and 112 | this will be needed if you are using Docker Machine. 113 | -------------------------------------------------------------------------------- /db_clickhouse.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.3.0" 4 | author = "Leonardo Cecchi " 5 | description = "ClickHouse Nim interface" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.19.0" 12 | -------------------------------------------------------------------------------- /src/db_clickhouse.nim: -------------------------------------------------------------------------------- 1 | import httpclient, uri, system, strutils, sequtils, httpcore 2 | 3 | type 4 | DbConn* = ref object 5 | hostName: string ## We connect to this host name 6 | httpPort: int ## and using this port 7 | client: HttpClient 8 | 9 | ## This error is raised when the query execution raised an error 10 | ## in the clickhouse engine 11 | DbError* = object of IOError 12 | 13 | ## A row 14 | Row* = seq[string] 15 | 16 | ## Encode a string in the TabSeparated format 17 | proc encodeString*(arg: string): string = 18 | result = "" 19 | for i in 0..len(arg)-1: 20 | if arg[i] == '\t': 21 | result &= "\\t" 22 | elif arg[i] == '\b': 23 | result &= "\\b" 24 | elif arg[i] == '\f': 25 | result &= "\\f" 26 | elif arg[i] == '\r': 27 | result &= "\\r" 28 | elif arg[i] == '\n': 29 | result &= "\\n" 30 | elif arg[i] == '\0': 31 | result &= "\\0" 32 | elif arg[i] == '\'': 33 | result &= "\\'" 34 | elif arg[i] == '\\': 35 | result &= "\\\\" 36 | else: 37 | result &= arg[i] 38 | 39 | ## The following functions implement the TabSeparated format 40 | ## --------------------------------------------------------- 41 | 42 | ## Encode a row in the TabSeparated format 43 | proc encodeRow*(args:varargs[string, `$`]): string = 44 | result = join(map(args, encodeString), "\t") 45 | 46 | ## Encode a row in the TabSeparated format 47 | proc encodeRow*(args:seq[string]): string = 48 | result = join(map(args, encodeString), "\t") 49 | 50 | ## Encode a list of rows in the TabSeparated format 51 | proc encodeRows*(data:seq[seq[string]]): string = 52 | result = join(map(data, encodeRow), "\n") 53 | 54 | ## Decode a string 55 | proc decodeString*(content: string): string = 56 | result = content 57 | while true: 58 | let idx = result.find("\\") 59 | if idx == -1: 60 | break 61 | if idx == result.len() - 1: 62 | break 63 | if result[idx+1] == 'n': 64 | result = result[0.. 0) 54 | check(result[0].len() == 2) 55 | check(result[0] == @["0", "1"]) 56 | 57 | test "get first row": 58 | let row = client.getRow("SELECT * FROM test_table ORDER BY test_column") 59 | check(row.len() == 2) 60 | check(row == @["0", "1"]) 61 | 62 | test "get first row": 63 | let value = client.getValue("SELECT * FROM test_table ORDER BY test_column") 64 | check(value == "0") 65 | 66 | test "exec raw data": 67 | discard client.execRaw("INSERT INTO test_table FORMAT TabSeparated", "first\tsecond") 68 | check(client.execRaw("SELECT * FROM test_table FORMAT TabSeparated").splitLines().len() > 2) 69 | 70 | test "get all rows returns the correct number of rows": 71 | client.exec("TRUNCATE test_table") 72 | client.exec("INSERT INTO test_table FORMAT TabSeparated", "first string", "second string") 73 | client.exec("INSERT INTO test_table FORMAT TabSeparated", "third string", "fourth string") 74 | let result = client.getAllRows("SELECT * FROM test_table ORDER BY test_column") 75 | check(result.len() == 2) 76 | 77 | 78 | suite "TabSeparated encoding tests": 79 | test "basic string decoding": 80 | check(decodeString("ciao") == "ciao") 81 | check(decodeString("ci\\tao") == "ci\tao") 82 | 83 | test "basic row decoding": 84 | check(decodeRow("ciao\tda\tm\\te") == @["ciao", "da", "m\te"]) 85 | 86 | test "basic data decoding": 87 | check(decodeRows("ciao\tda\tme\nprova\tper\tte") == @[@["ciao", "da", "me"], @["prova", "per", "te"]]) 88 | 89 | test "basic string encoding": 90 | check(encodeString("cia\to") == "cia\\to") 91 | 92 | test "basic row encoding": 93 | check(encodeRow(1, "ciao", 3) == "1\tciao\t3") 94 | 95 | test "basic table encoding": 96 | check(encodeRows(@[@["one", "two"], @["three", "four"]]) == "one\ttwo\nthree\tfour") 97 | 98 | test "extract first row": 99 | check(extractFirstRow("test") == "test") 100 | check(extractFirstRow("test\ttwo\nthree\tfour") == "test\ttwo") 101 | 102 | test "extract first column": 103 | check(extractFirstCol("test") == "test") 104 | check(extractFirstCol("test\ttwo") == "test") -------------------------------------------------------------------------------- /tests/test_clickhouse.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") --------------------------------------------------------------------------------