click_house 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2d62b43147b81ad3551c48c79388a949307d03491c1d87965d25ae9818e0a86
4
- data.tar.gz: ad6e79c3a974e4e39889d93dcabd94d3dc8f2bced9b739449f741bd1c8c83038
3
+ metadata.gz: f271ed936f2d0f66ec2045ec72025e317523dc8d70123c2bcf145ede5b4f1871
4
+ data.tar.gz: d3e48437b08cbf5eb96315bd46ba88429302bcbebc882d787257c3636c586c8f
5
5
  SHA512:
6
- metadata.gz: 9035db18ecc851ab6de8f9ae88d434dfff07f2da5e23eac4cf3c091fe699871c64f0c9c05a1637730f44dfd5fef4eca60f6cd94f613360113152461f7e4dabe8
7
- data.tar.gz: bb93033b9ffc82a7003af0affea015ad2a84292a86a50c46ea6cecfa232b22d172708dc1633f896fd29eea373270eb464baa70e02df8d036195d0aeb2f88f29c
6
+ metadata.gz: 3bae70c6d33a93ad298db0d14b91beb4bbfe31117cb51e9a58cdeeb95dc48e506363d3e660e6b9e3cd54398df2065210f3b0582e1891bb76e2bdd900520269b3
7
+ data.tar.gz: cb202cf091808068383a32bb3a2a078aaacc3e051e5c30ecfc47d0bdfab84bb188f0ace4dbc6dbd2950407e5743deeb5f2ffd82c3b31a27ef6fcea01a3fdab1d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ # 2.1.0
2
+ * `ClickHouse.connection.insert` now returns `ClickHouse::Response::Execution` objects
3
+ with methods `headers`, `summary`, `written_rows`, `written_bytes`, etc...
4
+ * `ClickHouse.connection.insert(columns: ["id"], values: [1])` now uses `JSONCompactEachRow` by default
5
+ (to increase JSON serialization speed)
6
+ * Methods `insert_rows` and `insert_compact` added to `connection`
7
+ * Added ability to pass object directly to insert like:
8
+ `ClickHouse.connection.insert("table", {id: 1})` or
9
+ `ClickHouse.connection.insert("table", [{id: 1})]` (for ruby < 3.0 use `ClickHouse.connection.insert("table", [{id: 1}], {})`)
10
+ * 🔥 Added config option `json_serializer` (one of `ClickHouse::Serializer::JsonSerializer`, `ClickHouse::Serializer::JsonOjSerializer`)
11
+ * 🔥 Added config option `symbolize_keys`
12
+ * 🔥 Added type serialization for INSERT statements, example below:
13
+
14
+ ```sql
15
+ CREATE TABLE assets(visible Boolean, tags Array(Nullable(String))) ENGINE Memory
16
+ ```
17
+
18
+ ```ruby
19
+ # cache table schema in a class variable
20
+ @schema = ClickHouse.connection.table_schema('assets')
21
+
22
+ # Json each row
23
+ ClickHouse.connection.insert('assets', @schema.serialize({'visible' => true, 'tags' => ['ruby']}))
24
+
25
+ # Json compact
26
+ ClickHouse.connection.insert('assets', columns: %w[visible tags]) do |buffer|
27
+ buffer << [
28
+ @schema.serialize_column("visible", true),
29
+ @schema.serialize_column("tags", ['ruby']),
30
+ ]
31
+ end
32
+ ```
33
+
1
34
  # 2.0.0
2
35
  * Fixed `Bigdecimal` casting with high precision
3
36
  * Added nested `type casting like Array(Array(Array(Nullable(T))))`
@@ -5,6 +38,7 @@
5
38
  * Added `Tuple(T1, T2)` support
6
39
  * Added support for `Faraday` v1 and v2
7
40
  * Added support for `Oj` parser
41
+ * Time types return `Time` class instead of `DateTime` for now
8
42
 
9
43
  # 1.6.3
10
44
  * [PR](https://github.com/shlima/click_house/pull/38) Add option format for insert
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- click_house (2.0.2)
4
+ click_house (2.1.0)
5
5
  activesupport
6
6
  faraday (>= 1.7, < 3)
7
7
 
data/Gemfile_faraday1 CHANGED
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
5
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  # lock faraday to v1
8
- gem 'faraday_middleware'
8
+ gem 'faraday', '< 2'
9
9
 
10
10
  # Specify your gem's dependencies in click_house.gemspec
11
11
  gemspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- click_house (2.0.1)
4
+ click_house (2.1.0)
5
5
  activesupport
6
6
  faraday (>= 1.7, < 3)
7
7
 
@@ -40,8 +40,6 @@ GEM
40
40
  faraday-patron (1.0.0)
41
41
  faraday-rack (1.0.0)
42
42
  faraday-retry (1.0.3)
43
- faraday_middleware (1.2.0)
44
- faraday (~> 1.0)
45
43
  i18n (1.12.0)
46
44
  concurrent-ruby (~> 1.0)
47
45
  json (2.6.2)
@@ -94,12 +92,13 @@ GEM
94
92
  unicode-display_width (2.3.0)
95
93
 
96
94
  PLATFORMS
95
+ ruby
97
96
  x86_64-darwin-21
98
97
 
99
98
  DEPENDENCIES
100
99
  bundler
101
100
  click_house!
102
- faraday_middleware
101
+ faraday (< 2)
103
102
  oj
104
103
  pry
105
104
  rake
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- click_house (1.7.0)
4
+ click_house (2.1.0)
5
5
  activesupport
6
6
  faraday (>= 1.7, < 3)
7
7
 
data/README.md CHANGED
@@ -52,6 +52,8 @@ ClickHouse.config do |config|
52
52
  config.timeout = 60
53
53
  config.open_timeout = 3
54
54
  config.ssl_verify = false
55
+ # set to true to symbolize keys for SELECT and INSERT statements (type casting)
56
+ config.symbolize_keys = false
55
57
  config.headers = {}
56
58
 
57
59
  # or provide connection options separately
@@ -66,10 +68,15 @@ ClickHouse.config do |config|
66
68
  # if you want to add settings to all queries
67
69
  config.global_params = { mutations_sync: 1 }
68
70
 
69
- # choose a ruby JSON parser
71
+ # choose a ruby JSON parser (default one)
70
72
  config.json_parser = ClickHouse::Middleware::ParseJson
71
73
  # or Oj parser
72
74
  config.json_parser = ClickHouse::Middleware::ParseJsonOj
75
+
76
+ # JSON.dump (default one)
77
+ config.json_serializer = ClickHouse::Serializer::JsonSerializer
78
+ # or Oj.dump
79
+ config.json_serializer = ClickHouse::Serializer::JsonOjSerializer
73
80
  end
74
81
  ```
75
82
 
@@ -195,7 +202,8 @@ response.body #=> "\u0002\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
195
202
 
196
203
  ## Insert
197
204
 
198
- When column names and values are transferred separately
205
+ When column names and values are transferred separately, data sends to the server
206
+ using `JSONCompactEachRow` format by default.
199
207
 
200
208
  ```ruby
201
209
  ClickHouse.connection.insert('table', columns: %i[id name]) do |buffer|
@@ -203,23 +211,42 @@ ClickHouse.connection.insert('table', columns: %i[id name]) do |buffer|
203
211
  buffer << [2, 'Venus']
204
212
  end
205
213
 
214
+ # or
206
215
  ClickHouse.connection.insert('table', columns: %i[id name], values: [[1, 'Mercury'], [2, 'Venus']])
207
- #=> true
208
216
  ```
209
217
 
210
- When rows are passed as a hash
211
-
218
+ When rows are passed as an Array or a Hash, data sends to the server
219
+ using `JSONEachRow` format by default.
212
220
 
213
221
  ```ruby
214
- ClickHouse.connection.insert('table', values: [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }])
222
+ ClickHouse.connection.insert('table', [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }])
223
+
224
+ # or
225
+ ClickHouse.connection.insert('table', { name: 'Sun', id: 1 })
226
+
227
+ # for ruby < 3.0 provide an extra argument
228
+ ClickHouse.connection.insert('table', { name: 'Sun', id: 1 }, {})
215
229
 
230
+ # or
216
231
  ClickHouse.connection.insert('table') do |buffer|
217
232
  buffer << { name: 'Sun', id: 1 }
218
233
  buffer << { name: 'Moon', id: 2 }
219
234
  end
220
- #=> true
221
235
  ```
222
236
 
237
+ Sometimes it's needed to use other format than `JSONEachRow` For example if you want to send BigDecimal's
238
+ you could use `JSONStringsEachRow` format so string representation of `BigDecimal` will be parsed:
239
+
240
+ ```ruby
241
+ ClickHouse.connection.insert('table', { name: 'Sun', id: '1' }, format: 'JSONStringsEachRow')
242
+ # or
243
+ ClickHouse.connection.insert_rows('table', { name: 'Sun', id: '1' }, format: 'JSONStringsEachRow')
244
+ # or
245
+ ClickHouse.connection.insert_compact('table', columns: %w[name id], values: %w[Sun 1], format: 'JSONCompactStringsEachRow')
246
+ ```
247
+
248
+ See the [type casting](#type-casting) section to insert the data in a proper way.
249
+
223
250
  ## Create a table
224
251
  ### Create table using DSL
225
252
 
@@ -326,42 +353,35 @@ end
326
353
  ## Type casting
327
354
 
328
355
  By default gem provides all necessary type casting, but you may overwrite or define
329
- your own logic
356
+ your own logic. if you need to redefine all built-in types with your implementation,
357
+ just clear the default type system:
330
358
 
331
359
  ```ruby
332
- class DateType
333
- def cast(value)
334
- Date.parse(value)
335
- end
336
-
337
- def serialize(value)
338
- value.strftime('%Y-%m-%d')
339
- end
340
- end
341
-
342
- ClickHouse.add_type('Date', DateType.new)
360
+ ClickHouse.types.clear
361
+ ClickHouse.types # => {}
362
+ ClickHouse.types.default #=> #<ClickHouse::Type::UndefinedType>
343
363
  ```
344
364
 
345
- If native type supports arguments, define *String* type with `%s`
346
- argument and *Numeric* type with `%d` argument:
365
+ Type casting works automatically when fetching data, when inserting data, you must serialize the types yourself
347
366
 
348
- ```ruby
349
- class DateTimeType
350
- def cast(value, time_zone)
351
- Time.parse("#{value} #{time_zone}")
352
- end
353
- end
354
-
355
- ClickHouse.add_type('DateTime(%s)', DateTimeType.new)
367
+ ```sql
368
+ CREATE TABLE assets(visible Boolean, tags Array(Nullable(String))) ENGINE Memory
356
369
  ```
357
370
 
358
- if you need to redefine all built-in types with your implementation,
359
- just clear the default type system:
360
-
361
371
  ```ruby
362
- ClickHouse.types.clear
363
- ClickHouse.types # => {}
364
- ClickHouse.types.default #=> #<ClickHouse::Type::UndefinedType:0x00007fc1cfabd630>
372
+ # cache table schema in a class variable
373
+ @schema = ClickHouse.connection.table_schema('assets')
374
+
375
+ # Json each row
376
+ ClickHouse.connection.insert('assets', @schema.serialize({'visible' => true, 'tags' => ['ruby']}))
377
+
378
+ # Json compact
379
+ ClickHouse.connection.insert('assets', columns: %w[visible tags]) do |buffer|
380
+ buffer << [
381
+ @schema.serialize_column("visible", true),
382
+ @schema.serialize_column("tags", ['ruby']),
383
+ ]
384
+ end
365
385
  ```
366
386
 
367
387
  ## Using with a connection pool
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'stringio'
5
+
6
+ INPUT = Array.new(5_000_000, 'foo bar')
7
+
8
+ Benchmark.bm do |x|
9
+ x.report('map.join') do
10
+ INPUT.map(&:to_s).join("\n")
11
+ end
12
+
13
+ x.report('StringIO') do
14
+ out = StringIO.new
15
+ INPUT.each do |value|
16
+ out << "#{value}\n"
17
+ end
18
+ out.string
19
+ end
20
+ end
@@ -18,18 +18,21 @@ module ClickHouse
18
18
  headers: {},
19
19
  global_params: {},
20
20
  json_parser: ClickHouse::Middleware::ParseJson,
21
+ json_serializer: ClickHouse::Serializer::JsonSerializer,
22
+ oj_dump_options: {
23
+ mode: :compat # to be able to dump improper JSON like {1 => 2}
24
+ },
21
25
  oj_load_options: {
22
26
  mode: :custom,
23
27
  allow_blank: true,
24
28
  bigdecimal_as_decimal: false, # dump BigDecimal as a String
25
29
  bigdecimal_load: :bigdecimal, # convert all decimal numbers to BigDecimal
26
- empty_string: false,
27
- second_precision: 6,
28
- time_format: :ruby,
29
30
  },
30
31
  json_load_options: {
31
32
  decimal_class: BigDecimal,
32
33
  },
34
+ # should be after json load options
35
+ symbolize_keys: false,
33
36
  }.freeze
34
37
 
35
38
  attr_accessor :adapter
@@ -49,6 +52,9 @@ module ClickHouse
49
52
  attr_accessor :oj_load_options
50
53
  attr_accessor :json_load_options
51
54
  attr_accessor :json_parser # response middleware
55
+ attr_accessor :oj_dump_options
56
+ attr_accessor :json_serializer # [ClickHouse::Serializer::Base]
57
+ attr_accessor :symbolize_keys # [NilClass, Boolean]
52
58
 
53
59
  def initialize(params = {})
54
60
  assign(DEFAULTS.merge(params))
@@ -77,5 +83,25 @@ module ClickHouse
77
83
  def null_logger
78
84
  @null_logger ||= Logger.new(IO::NULL)
79
85
  end
86
+
87
+ # @param klass [ClickHouse::Serializer::Base]
88
+ def json_serializer=(klass)
89
+ @json_serializer = klass.new(self)
90
+ end
91
+
92
+ def symbolize_keys=(value)
93
+ bool = value ? true : false
94
+
95
+ # merge to be able to clone a config
96
+ # prevent overriding default values
97
+ self.oj_load_options = oj_load_options.merge(symbol_keys: bool)
98
+ self.json_load_options = json_load_options.merge(symbolize_names: bool)
99
+ @symbolize_keys = bool
100
+ end
101
+
102
+ # @param name [Symbol, String]
103
+ def key(name)
104
+ symbolize_keys ? name.to_sym : name.to_s
105
+ end
80
106
  end
81
107
  end
@@ -5,4 +5,5 @@ module ClickHouse
5
5
  NetworkException = Class.new(Error)
6
6
  DbException = Class.new(Error)
7
7
  StatementException = Class.new(Error)
8
+ SerializeError = Class.new(Error)
8
9
  end
@@ -3,7 +3,8 @@
3
3
  module ClickHouse
4
4
  module Extend
5
5
  module ConnectionInserting
6
- EMPTY_INSERT = true
6
+ DEFAULT_JSON_EACH_ROW_FORMAT = 'JSONEachRow'
7
+ DEFAULT_JSON_COMPACT_EACH_ROW_FORMAT = 'JSONCompactEachRow'
7
8
 
8
9
  # @return [Boolean]
9
10
  #
@@ -13,20 +14,71 @@ module ClickHouse
13
14
  # buffer << ['Moon', 2]
14
15
  # end
15
16
  #
16
- # == Example with a param
17
- # subject.insert('rspec', values: [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }], format: 'JSONStringsEachRow')
18
- def insert(table, columns: [], values: [], format: 'JSONEachRow')
19
- yield(values) if block_given?
17
+ # @return [Response::Execution]
18
+ # @param body [Array, Hash]
19
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
20
+ def insert(table, body = [], **opts)
21
+ # In Ruby < 3.0, if the last argument is a hash, and the method being called
22
+ # accepts keyword arguments, then it is always converted to keyword arguments.
23
+ columns = opts.fetch(:columns, [])
24
+ values = opts.fetch(:values, [])
25
+ format = opts.fetch(:format, nil)
26
+
27
+ yield(body) if block_given?
20
28
 
21
- body = if columns.empty?
22
- values.map(&:to_json)
29
+ # values: [{id: 1}]
30
+ if values.any? && columns.empty?
31
+ return insert_rows(table, values, format: format)
32
+ end
33
+
34
+ # body: [{id: 1}]
35
+ if body.any? && columns.empty?
36
+ return insert_rows(table, body, format: format)
37
+ end
38
+
39
+ # body: [1], columns: ["id"]
40
+ if body.any? && columns.any?
41
+ return insert_compact(table, columns: columns, values: body, format: format)
42
+ end
43
+
44
+ # columns: ["id"], values: [[1]]
45
+ if columns.any? && values.any?
46
+ return insert_compact(table, columns: columns, values: values, format: format)
47
+ end
48
+
49
+ Response::Factory.empty_exec
50
+ end
51
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
52
+
53
+ # @param table [String]
54
+ # @param body [Array, Hash]
55
+ # @param format [String]
56
+ # @return [Response::Execution]
57
+ #
58
+ # Sometimes it's needed to use other format than JSONEachRow
59
+ # For example if you want to send BigDecimal's you could use
60
+ # JSONStringsEachRow format so string representation of BigDecimal will be parsed
61
+ def insert_rows(table, body, format: nil)
62
+ format ||= DEFAULT_JSON_EACH_ROW_FORMAT
63
+
64
+ case body
65
+ when Hash
66
+ Response::Factory.exec(execute("INSERT INTO #{table} FORMAT #{format}", config.json_serializer.dump(body)))
67
+ when Array
68
+ Response::Factory.exec(execute("INSERT INTO #{table} FORMAT #{format}", config.json_serializer.dump_each_row(body)))
23
69
  else
24
- values.map { |value_row| columns.zip(value_row).to_h.to_json }
70
+ raise ArgumentError, "unknown body class <#{body.class}>"
25
71
  end
72
+ end
26
73
 
27
- return EMPTY_INSERT if values.empty?
74
+ # @return [Response::Execution]
75
+ def insert_compact(table, columns: [], values: [], format: nil)
76
+ format ||= DEFAULT_JSON_COMPACT_EACH_ROW_FORMAT
77
+
78
+ yield(values) if block_given?
28
79
 
29
- execute("INSERT INTO #{table} FORMAT #{format}", body.join("\n")).success?
80
+ response = execute("INSERT INTO #{table} (#{columns.join(',')}) FORMAT #{format}", config.json_serializer.dump_each_row(values))
81
+ Response::Factory.exec(response)
30
82
  end
31
83
  end
32
84
  end
@@ -6,17 +6,17 @@ module ClickHouse
6
6
  # @return [ResultSet]
7
7
  def select_all(sql)
8
8
  response = get(body: sql, query: { default_format: 'JSON' })
9
- Response::Factory[response]
9
+ Response::Factory.response(response, config)
10
10
  end
11
11
 
12
12
  def select_value(sql)
13
13
  response = get(body: sql, query: { default_format: 'JSON' })
14
- Array(Response::Factory[response].first).dig(0, -1)
14
+ Array(Response::Factory.response(response, config).first).dig(0, -1)
15
15
  end
16
16
 
17
17
  def select_one(sql)
18
18
  response = get(body: sql, query: { default_format: 'JSON' })
19
- Response::Factory[response].first
19
+ Response::Factory.response(response, config).first
20
20
  end
21
21
  end
22
22
  end
@@ -10,7 +10,12 @@ module ClickHouse
10
10
 
11
11
  # @return [ResultSet]
12
12
  def describe_table(name)
13
- Response::Factory[execute("DESCRIBE TABLE #{name} FORMAT JSON")]
13
+ Response::Factory.response(execute("DESCRIBE TABLE #{name} FORMAT JSON"), config)
14
+ end
15
+
16
+ # @return [ResultSet]
17
+ def table_schema(name)
18
+ Response::Factory.response(execute("SELECT * FROM #{name} WHERE 1=0 FORMAT JSON"), config)
14
19
  end
15
20
 
16
21
  # @return [Boolean]
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Response
5
+ class Execution
6
+ SUMMARY_HEADER = 'x-clickhouse-summary'
7
+
8
+ attr_reader :headers, :summary
9
+
10
+ # @param headers [Faraday::Utils::Headers]
11
+ def initialize(headers: Faraday::Utils::Headers.new)
12
+ @headers = headers
13
+ @summary = parse_summary(headers[SUMMARY_HEADER])
14
+ end
15
+
16
+ # @return [Integer]
17
+ def read_rows
18
+ summary['read_rows'].to_i
19
+ end
20
+
21
+ # @return [Integer]
22
+ def read_bytes
23
+ summary['read_bytes'].to_i
24
+ end
25
+
26
+ # @return [Integer]
27
+ def written_rows
28
+ summary['written_rows'].to_i
29
+ end
30
+
31
+ # @return [Integer]
32
+ def written_bytes
33
+ summary['written_bytes'].to_i
34
+ end
35
+
36
+ # @return [Integer]
37
+ def total_rows_to_read
38
+ summary['total_rows_to_read'].to_i
39
+ end
40
+
41
+ # @return [Integer]
42
+ def result_rows
43
+ summary['result_rows'].to_i
44
+ end
45
+
46
+ # @return [Integer]
47
+ def result_bytes
48
+ summary['result_bytes'].to_i
49
+ end
50
+
51
+ private
52
+
53
+ # @return [Hash]
54
+ # {
55
+ # "read_rows" => "1",
56
+ # "read_bytes" => "23",
57
+ # "written_rows" => "1",
58
+ # "written_bytes" => "23",
59
+ # "total_rows_to_read" => "0",
60
+ # "result_rows" => "1",
61
+ # "result_bytes" => "23",
62
+ # }
63
+ def parse_summary(value)
64
+ return {} if value.nil? || value.empty?
65
+
66
+ JSON.parse(value)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -3,21 +3,41 @@
3
3
  module ClickHouse
4
4
  module Response
5
5
  class Factory
6
- # @return [String, ResultSet]
7
- # @params env [Faraday::Response]
8
- def self.[](faraday)
6
+ KEY_META = 'meta'
7
+ KEY_DATA = 'data'
8
+ KEY_TOTALS = 'totals'
9
+ KEY_STATISTICS = 'statistics'
10
+ KEY_ROWS_BEFORE_LIMIT_AT_LEAST = 'rows_before_limit_at_least'
11
+
12
+ # @return [Nil], ResultSet]
13
+ # @params faraday [Faraday::Response]
14
+ # @params config [Config]
15
+ def self.response(faraday, config)
9
16
  body = faraday.body
10
17
 
11
- return body if !body.is_a?(Hash) || !(body.key?('meta') && body.key?('data'))
18
+ return body unless body.is_a?(Hash)
19
+ return body unless body.key?(config.key(KEY_META)) && body.key?(config.key(KEY_DATA))
12
20
 
13
21
  ResultSet.new(
14
- meta: body.fetch('meta'),
15
- data: body.fetch('data'),
16
- totals: body['totals'],
17
- statistics: body['statistics'],
18
- rows_before_limit_at_least: body['rows_before_limit_at_least']
22
+ config: config,
23
+ meta: body.fetch(config.key(KEY_META)),
24
+ data: body.fetch(config.key(KEY_DATA)),
25
+ totals: body[config.key(KEY_TOTALS)],
26
+ statistics: body[config.key(KEY_STATISTICS)],
27
+ rows_before_limit_at_least: body[config.key(KEY_ROWS_BEFORE_LIMIT_AT_LEAST)]
19
28
  )
20
29
  end
30
+
31
+ # @return [Response::Execution]
32
+ # @params faraday [Faraday::Response]
33
+ def self.exec(faraday)
34
+ Execution.new(headers: faraday.headers)
35
+ end
36
+
37
+ # @return [Response::Execution]
38
+ def self.empty_exec
39
+ Execution.new
40
+ end
21
41
  end
22
42
  end
23
43
  end
@@ -6,27 +6,63 @@ module ClickHouse
6
6
  extend Forwardable
7
7
  include Enumerable
8
8
 
9
- PLACEHOLDER_D = '%d'
10
- PLACEHOLDER_S = '%s'
9
+ KEY_META_NAME = 'name'
10
+ KEY_META_TYPE = 'type'
11
11
 
12
12
  def_delegators :to_a,
13
13
  :inspect, :each, :fetch, :length, :count, :size,
14
14
  :first, :last, :[], :to_h
15
15
 
16
- attr_reader :meta, :data, :totals, :statistics, :rows_before_limit_at_least
16
+ attr_reader :config, :meta, :data, :totals, :statistics, :rows_before_limit_at_least
17
17
 
18
+ # @param config [Config]
18
19
  # @param meta [Array]
19
20
  # @param data [Array]
20
21
  # @param totals [Array|Hash|NilClass] Support for 'GROUP BY WITH TOTALS' modifier
21
22
  # https://clickhouse.tech/docs/en/sql-reference/statements/select/group-by/#with-totals-modifier
22
23
  # Hash in JSON format and Array in JSONCompact
23
- def initialize(meta:, data:, totals: nil, statistics: nil, rows_before_limit_at_least: nil)
24
+ # rubocop:disable Metrics/ParameterLists
25
+ def initialize(config:, meta:, data:, totals: nil, statistics: nil, rows_before_limit_at_least: nil)
26
+ @config = config
24
27
  @meta = meta
25
28
  @data = data
26
29
  @totals = totals
27
30
  @rows_before_limit_at_least = rows_before_limit_at_least
28
31
  @statistics = Hash(statistics)
29
32
  end
33
+ # rubocop:enable Metrics/ParameterLists
34
+
35
+ # @return [Array, Hash]
36
+ # @param data [Array, Hash]
37
+ def serialize(data)
38
+ case data
39
+ when Hash
40
+ serialize_one(data)
41
+ when Array
42
+ data.map(&method(:serialize_one))
43
+ else
44
+ raise ArgumentError, "expect Hash or Array, got: #{data.class}"
45
+ end
46
+ end
47
+
48
+ # @return [Hash]
49
+ # @param row [Hash]
50
+ def serialize_one(row)
51
+ row.each_with_object({}) do |(key, value), object|
52
+ object[key] = serialize_column(key, value)
53
+ end
54
+ end
55
+
56
+ # @param name [String] column name
57
+ # @param value [Any]
58
+ def serialize_column(name, value)
59
+ stmt = types.fetch(name)
60
+ serialize_type(stmt, value)
61
+ rescue KeyError => e
62
+ raise SerializeError, "field <#{name}> does not exists in table schema: #{types}", e.backtrace
63
+ rescue StandardError => e
64
+ raise SerializeError, "failed to serialize <#{name}> with #{stmt}, #{e.class}, #{e.message}", e.backtrace
65
+ end
30
66
 
31
67
  def to_a
32
68
  @to_a ||= data.each do |row|
@@ -39,8 +75,11 @@ module ClickHouse
39
75
  # @return [Hash<String, Ast::Statement>]
40
76
  def types
41
77
  @types ||= meta.each_with_object({}) do |row, object|
42
- object[row.fetch('name')] = begin
43
- current = Ast::Parser.new(row.fetch('type')).parse
78
+ column = row.fetch(config.key(KEY_META_NAME))
79
+ # make symbol keys, if config.symbolize_keys is true,
80
+ # to be able to cast and serialize properly
81
+ object[config.key(column)] = begin
82
+ current = Ast::Parser.new(row.fetch(config.key(KEY_META_TYPE))).parse
44
83
  assign_type(current)
45
84
  current
46
85
  end
@@ -96,6 +135,42 @@ module ClickHouse
96
135
  cast_type(stmt.arguments.fetch(ix), item)
97
136
  end
98
137
  end
138
+
139
+ # @param stmt [Ast::Statement]
140
+ def serialize_type(stmt, value)
141
+ return serialize_container(stmt, value) if stmt.caster.container?
142
+ return serialize_map(stmt, value) if stmt.caster.map?
143
+ return serialize_tuple(stmt, Array(value)) if stmt.caster.tuple?
144
+
145
+ stmt.caster.serialize(value, *stmt.arguments.map(&:value))
146
+ end
147
+
148
+ # @param stmt [Ast::Statement]
149
+ def serialize_container(stmt, value)
150
+ stmt.caster.cast_each(value) do |item|
151
+ # TODO: raise an error if multiple arguments
152
+ serialize_type(stmt.arguments.first, item)
153
+ end
154
+ end
155
+
156
+ # @return [Hash]
157
+ # @param stmt [Ast::Statement]
158
+ # @param hash [Hash]
159
+ def serialize_map(stmt, hash)
160
+ raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash)
161
+
162
+ key_type, value_type = stmt.arguments
163
+ hash.each_with_object({}) do |(key, value), object|
164
+ object[serialize_type(key_type, key)] = serialize_type(value_type, value)
165
+ end
166
+ end
167
+
168
+ # @param stmt [Ast::Statement]
169
+ def serialize_tuple(stmt, value)
170
+ value.map.with_index do |item, ix|
171
+ serialize_type(stmt.arguments.fetch(ix), item)
172
+ end
173
+ end
99
174
  end
100
175
  end
101
176
  end
@@ -4,6 +4,6 @@ module ClickHouse
4
4
  module Response
5
5
  autoload :Factory, 'click_house/response/factory'
6
6
  autoload :ResultSet, 'click_house/response/result_set'
7
- autoload :Tokenize, 'click_house/response/tokenize'
7
+ autoload :Execution, 'click_house/response/execution'
8
8
  end
9
9
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Serializer
5
+ class Base
6
+ attr_reader :config
7
+
8
+ # @param config [Config]
9
+ def initialize(config)
10
+ @config = config
11
+ on_setup
12
+ end
13
+
14
+ def dump(data)
15
+ raise NotImplementedError, __method__
16
+ end
17
+
18
+ # @return [String]
19
+ # @param data [Array]
20
+ def dump_each_row(data, sep = "\n")
21
+ data.map(&method(:dump)).join(sep)
22
+ end
23
+
24
+ private
25
+
26
+ # require external dependencies here
27
+ def on_setup
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Serializer
5
+ class JsonOjSerializer < Base
6
+ def dump(data)
7
+ Oj.dump(data, config.oj_dump_options)
8
+ end
9
+
10
+ private
11
+
12
+ def on_setup
13
+ require 'oj' unless defined?(Oj)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Serializer
5
+ class JsonSerializer < Base
6
+ def dump(data)
7
+ JSON.dump(data)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Serializer
5
+ autoload :Base, 'click_house/serializer/base'
6
+ autoload :JsonSerializer, 'click_house/serializer/json_serializer'
7
+ autoload :JsonOjSerializer, 'click_house/serializer/json_oj_serializer'
8
+ end
9
+ end
@@ -7,6 +7,10 @@ module ClickHouse
7
7
  value.map(&block)
8
8
  end
9
9
 
10
+ def serialize_each(value, *_argv, &block)
11
+ value.map(&block)
12
+ end
13
+
10
14
  def container?
11
15
  true
12
16
  end
@@ -11,6 +11,10 @@ module ClickHouse
11
11
  raise NotImplementedError, __method__
12
12
  end
13
13
 
14
+ def serialize_each(_value, *)
15
+ raise NotImplementedError, __method__
16
+ end
17
+
14
18
  # @return [Boolean]
15
19
  # true if type contains another type like Nullable(T) or Array(T)
16
20
  def container?
@@ -7,7 +7,12 @@ module ClickHouse
7
7
  FALSE_VALUE = 0
8
8
 
9
9
  def cast(value)
10
- value.to_i == TRUE_VALUE
10
+ case value
11
+ when TrueClass, FalseClass
12
+ value
13
+ else
14
+ value.to_i == TRUE_VALUE
15
+ end
11
16
  end
12
17
 
13
18
  def serialize(value)
@@ -11,7 +11,7 @@ module ClickHouse
11
11
  end
12
12
  end
13
13
 
14
- def serialize(value, precision = 3)
14
+ def serialize(value, precision = 3, _tz = nil)
15
15
  value.strftime("%Y-%m-%d %H:%M:%S.%#{precision}N")
16
16
  end
17
17
  end
@@ -11,7 +11,7 @@ module ClickHouse
11
11
  end
12
12
  end
13
13
 
14
- def serialize(value)
14
+ def serialize(value, *)
15
15
  value.strftime('%Y-%m-%d %H:%M:%S')
16
16
  end
17
17
  end
@@ -3,14 +3,14 @@
3
3
  module ClickHouse
4
4
  module Type
5
5
  class DecimalType < BaseType
6
- MAXIMUM = Float::DIG
6
+ MAXIMUM = Float::DIG.next
7
7
 
8
8
  # clickhouse:
9
9
  # P - precision. Valid range: [ 1 : 76 ]. Determines how many decimal digits number can have (including fraction).
10
10
  # S - scale. Valid range: [ 0 : P ]. Determines how many decimal digits fraction can have.
11
11
  #
12
12
  # when Oj parser @refs https://stackoverflow.com/questions/47885304/deserialise-json-numbers-as-bigdecimal
13
- def cast(value, precision = Float::DIG, _scale = nil)
13
+ def cast(value, precision = MAXIMUM, _scale = nil)
14
14
  case value
15
15
  when BigDecimal
16
16
  value
@@ -21,8 +21,9 @@ module ClickHouse
21
21
  end
22
22
  end
23
23
 
24
- def serialize(value, precision = Float::DIG, _scale = nil)
25
- BigDecimal(value, precision.to_i).to_f unless value.nil?
24
+ # @return [BigDecimal]
25
+ def serialize(value, precision = MAXIMUM, _scale = nil)
26
+ cast(value, precision)
26
27
  end
27
28
  end
28
29
  end
@@ -7,7 +7,7 @@ module ClickHouse
7
7
  value.to_s unless value.nil?
8
8
  end
9
9
 
10
- def serialize(value)
10
+ def serialize(value, *)
11
11
  value.to_s unless value.nil?
12
12
  end
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClickHouse
4
- VERSION = '2.0.2'
4
+ VERSION = '2.1.0'
5
5
  end
data/lib/click_house.rb CHANGED
@@ -12,6 +12,7 @@ require 'active_support/core_ext/time/calculations'
12
12
  require 'click_house/version'
13
13
  require 'click_house/errors'
14
14
  require 'click_house/response'
15
+ require 'click_house/serializer'
15
16
  require 'click_house/type'
16
17
  require 'click_house/middleware'
17
18
  require 'click_house/extend'
@@ -33,6 +34,10 @@ module ClickHouse
33
34
  add_type 'LowCardinality', Type::LowCardinalityType.new
34
35
  add_type 'Tuple', Type::TupleType.new
35
36
 
37
+ %w[Bool].each do |column|
38
+ add_type column, Type::BooleanType.new
39
+ end
40
+
36
41
  %w[Date].each do |column|
37
42
  add_type column, Type::DateType.new
38
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: click_house
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aliaksandr Shylau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-19 00:00:00.000000000 Z
11
+ date: 2022-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -175,6 +175,7 @@ files:
175
175
  - lib/click_house/ast/parser.rb
176
176
  - lib/click_house/ast/statement.rb
177
177
  - lib/click_house/ast/ticker.rb
178
+ - lib/click_house/benchmark/map_join.rb
178
179
  - lib/click_house/config.rb
179
180
  - lib/click_house/connection.rb
180
181
  - lib/click_house/definition.rb
@@ -200,8 +201,13 @@ files:
200
201
  - lib/click_house/middleware/raise_error.rb
201
202
  - lib/click_house/middleware/response_base.rb
202
203
  - lib/click_house/response.rb
204
+ - lib/click_house/response/execution.rb
203
205
  - lib/click_house/response/factory.rb
204
206
  - lib/click_house/response/result_set.rb
207
+ - lib/click_house/serializer.rb
208
+ - lib/click_house/serializer/base.rb
209
+ - lib/click_house/serializer/json_oj_serializer.rb
210
+ - lib/click_house/serializer/json_serializer.rb
205
211
  - lib/click_house/type.rb
206
212
  - lib/click_house/type/array_type.rb
207
213
  - lib/click_house/type/base_type.rb