click_house 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67e6a326b4ad9972ca7713c935a6b4b1d03da9b3a1e4150119a6bd88cd63481e
4
- data.tar.gz: 5face2f1ed1c776b822e023fe862525b94cd3349055ce1a7135a0d184d493196
3
+ metadata.gz: f271ed936f2d0f66ec2045ec72025e317523dc8d70123c2bcf145ede5b4f1871
4
+ data.tar.gz: d3e48437b08cbf5eb96315bd46ba88429302bcbebc882d787257c3636c586c8f
5
5
  SHA512:
6
- metadata.gz: c3f9f7f4962e323684324fd863a487a46440aec742c06e8e406abe88c56f5457795e5b03d1367a206da45b3f912fb237428c1bc9251120e0f28272517d283a5a
7
- data.tar.gz: 70795ae32a9e0c555de49034fa19f3863459ea267b3e29aa85ff185f675dfacd15633d8c345f31d90ae2e6e95721187afc700fb40e891a6b5129ecc92ef87f6f
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.1)
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 (1.7.0)
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
@@ -47,13 +47,22 @@ module ClickHouse
47
47
  end
48
48
 
49
49
  # transport should work the same both with Faraday v1 and Faraday v2
50
+ # rubocop:disable Metrics/AbcSize
50
51
  def transport
51
52
  @transport ||= Faraday.new(config.url!) do |conn|
52
53
  conn.options.timeout = config.timeout
53
54
  conn.options.open_timeout = config.open_timeout
54
55
  conn.headers = config.headers
55
56
  conn.ssl.verify = config.ssl_verify
56
- conn.request(:basic_auth, config.username, config.password) if config.auth?
57
+
58
+ if config.auth?
59
+ if faraday_v1?
60
+ conn.request :basic_auth, config.username, config.password
61
+ else
62
+ conn.request :authorization, :basic, config.username, config.password
63
+ end
64
+ end
65
+
57
66
  conn.response Middleware::RaiseError
58
67
  conn.response Middleware::Logging, logger: config.logger!
59
68
  conn.response config.json_parser, content_type: %r{application/json}, parser_options: { config: config }
@@ -61,10 +70,16 @@ module ClickHouse
61
70
  conn.adapter config.adapter
62
71
  end
63
72
  end
73
+ # rubocop:enable Metrics/AbcSize
64
74
 
65
75
  def compose(path, query = {})
66
76
  # without <query.compact> "DB::Exception: Empty query" error will occur
67
77
  "#{path}?#{URI.encode_www_form({ send_progress_in_http_headers: 1 }.merge(query).compact)}"
68
78
  end
79
+
80
+ # @return [Boolean]
81
+ def faraday_v1?
82
+ Faraday::VERSION.start_with?('1')
83
+ end
69
84
  end
70
85
  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.1'
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.1
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