clickhouse-activerecord 0.5.14 → 0.6.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: 4ff54c1e4a5da2543e1a64ad8002822bf82ba5d3b6f7f74f00f1cb0ce23679dd
4
- data.tar.gz: 61158e185f4617e88701d2a9b1d261ddf7b4f9c7617454e66a899d55900a34a8
3
+ metadata.gz: 182d838abbda9972bdfa59b1e6a76af247bd4e3b275a5e611dfe6cd8ceaaea5c
4
+ data.tar.gz: f987ee429692c24518a0ea54d24f52c65f96ddfe1e6d6770fe0c2dd4549c623e
5
5
  SHA512:
6
- metadata.gz: 2d690a0cf443ce6e1b3f90e0443263e27b44be546fd080c11c8aa0278d6f630360371057880e667d4f5f76e31ac9bccf1b71fdfffeda09aa7d4d9a49af786f79
7
- data.tar.gz: f0f96caa0371b8206cee957e38b9f24993549eaa2dcc03265d4b398e051a0a9ebe32a7262c949bf4510dda3f843d8e9e2bb2437cc364dd9bf06f5a622e085530
6
+ metadata.gz: 90aac52260cd39eaa23dddb9be9290afa8ccb6731f4c84b6403d67fc56ddf1e24d075e9e33d9cc99ffdf29309608b0a8ae78a35db87d673731438dd5db50c57b
7
+ data.tar.gz: 505c299c7f657deb7b0685504c0f61754b801d0900238fa9a00a939b49873e379e2d2c84f9ce3661e0c26d405b9e94a31cbeb71c514f644cf4391059df44fa22
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Clickhouse::Activerecord
2
2
 
3
3
  A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
4
- Support ClickHouse version from 20.9 LTS.
4
+ Support ClickHouse version from 22.0 LTS.
5
5
 
6
6
  ## Installation
7
7
 
@@ -165,7 +165,7 @@ Structure load from `db/clickhouse_structure.sql` file:
165
165
 
166
166
  ```ruby
167
167
  Action.where(url: 'http://example.com', date: Date.current).where.not(name: nil).order(created_at: :desc).limit(10)
168
- # Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions WHERE actions.date = '2017-11-29' AND actions.url = 'http://example.com' AND (actions.name IS NOT NULL) ORDER BY actions.created_at DESC LIMIT 10
168
+ # Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions WHERE actions.date = '2017-11-29' AND actions.url = 'http://example.com' AND (actions.name IS NOT NULL) ORDER BY actions.created_at DESC LIMIT 10
169
169
  #=> #<ActiveRecord::Relation [#<Action *** >]>
170
170
 
171
171
  Action.create(url: 'http://example.com', date: Date.yesterday)
@@ -175,6 +175,14 @@ Action.create(url: 'http://example.com', date: Date.yesterday)
175
175
  ActionView.maximum(:date)
176
176
  # Clickhouse (10.3ms) SELECT maxMerge(actions.date) FROM actions
177
177
  #=> 'Wed, 29 Nov 2017'
178
+
179
+ Action.where(date: Date.current).final.limit(10)
180
+ # Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions FINAL WHERE actions.date = '2017-11-29' LIMIT 10
181
+ #=> #<ActiveRecord::Relation [#<Action *** >]>
182
+
183
+ Action.settings(optimize_read_in_order: 1).where(date: Date.current).limit(10)
184
+ # Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions FINAL WHERE actions.date = '2017-11-29' LIMIT 10 SETTINGS optimize_read_in_order = 1
185
+ #=> #<ActiveRecord::Relation [#<Action *** >]>
178
186
  ```
179
187
 
180
188
 
@@ -26,7 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'bundler', '>= 1.13.4'
27
27
  spec.add_runtime_dependency 'activerecord', '>= 5.2'
28
28
 
29
- spec.add_development_dependency 'bundler', '>= 1.15'
30
29
  spec.add_development_dependency 'rake', '~> 13.0'
31
30
  spec.add_development_dependency 'rspec', '~> 3.4'
32
31
  spec.add_development_dependency 'pry', '~> 0.12'
@@ -9,9 +9,8 @@ module ActiveRecord
9
9
  def serialize(value)
10
10
  value = super
11
11
  return unless value
12
- return value.strftime('%Y-%m-%d %H:%M:%S') unless value.acts_like?(:time)
13
12
 
14
- value.to_time.strftime('%Y-%m-%d %H:%M:%S')
13
+ value.strftime('%Y-%m-%d %H:%M:%S' + (@precision.present? && @precision > 0 ? ".%#{@precision}N" : ''))
15
14
  end
16
15
 
17
16
  def type_cast_from_database(value)
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
 
19
19
  def exec_query(sql, name = nil, binds = [], prepare: false)
20
20
  result = do_execute(sql, name)
21
- ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
21
+ ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'], result['meta'].map { |m| [m['name'], type_map.lookup(m['type'])] }.to_h)
22
22
  rescue ActiveRecord::ActiveRecordError => e
23
23
  raise e
24
24
  rescue StandardError => e
@@ -105,10 +105,20 @@ module ActiveRecord
105
105
  def process_response(res)
106
106
  case res.code.to_i
107
107
  when 200
108
- res.body.presence && JSON.parse(res.body)
108
+ if res.body.to_s.include?("DB::Exception")
109
+ raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
110
+ else
111
+ res.body.presence && JSON.parse(res.body)
112
+ end
109
113
  else
110
- raise ActiveRecord::ActiveRecordError,
111
- "Response code: #{res.code}:\n#{res.body}"
114
+ case res.body
115
+ when /DB::Exception:.*\(UNKNOWN_DATABASE\)/
116
+ raise ActiveRecord::NoDatabaseError
117
+ when /DB::Exception:.*\(DATABASE_ALREADY_EXISTS\)/
118
+ raise ActiveRecord::DatabaseAlreadyExists
119
+ else
120
+ raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
121
+ end
112
122
  end
113
123
  rescue JSON::ParserError
114
124
  res.body
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'clickhouse-activerecord/arel/visitors/to_sql'
4
- require 'clickhouse-activerecord/arel/table'
3
+ require 'arel/visitors/clickhouse'
4
+ require 'arel/nodes/settings'
5
5
  require 'clickhouse-activerecord/migration'
6
6
  require 'active_record/connection_adapters/clickhouse/oid/array'
7
7
  require 'active_record/connection_adapters/clickhouse/oid/date'
@@ -62,6 +62,8 @@ module ActiveRecord
62
62
 
63
63
  module ModelSchema
64
64
  module ClassMethods
65
+ delegate :final, :settings, to: :all
66
+
65
67
  def is_view
66
68
  @is_view || false
67
69
  end
@@ -69,10 +71,10 @@ module ActiveRecord
69
71
  def is_view=(value)
70
72
  @is_view = value
71
73
  end
72
-
73
- def arel_table # :nodoc:
74
- @arel_table ||= ClickhouseActiverecord::Arel::Table.new(table_name, type_caster: type_caster)
75
- end
74
+ #
75
+ # def arel_table # :nodoc:
76
+ # @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
77
+ # end
76
78
  end
77
79
  end
78
80
 
@@ -92,7 +94,7 @@ module ActiveRecord
92
94
  datetime: { name: 'DateTime' },
93
95
  datetime64: { name: 'DateTime64' },
94
96
  date: { name: 'Date' },
95
- boolean: { name: 'UInt8' },
97
+ boolean: { name: 'Bool' },
96
98
  uuid: { name: 'UUID' },
97
99
 
98
100
  enum8: { name: 'Enum8' },
@@ -145,7 +147,7 @@ module ActiveRecord
145
147
  end
146
148
 
147
149
  def arel_visitor # :nodoc:
148
- ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
150
+ Arel::Visitors::Clickhouse.new(self)
149
151
  end
150
152
 
151
153
  def native_database_types #:nodoc:
@@ -191,7 +193,7 @@ module ActiveRecord
191
193
  super
192
194
  register_class_with_limit m, %r(String), Type::String
193
195
  register_class_with_limit m, 'Date', Clickhouse::OID::Date
194
- register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
196
+ register_class_with_precision m, %r(datetime)i, Clickhouse::OID::DateTime
195
197
 
196
198
  register_class_with_limit m, %r(Int8), Type::Integer
197
199
  register_class_with_limit m, %r(Int16), Type::Integer
@@ -0,0 +1,11 @@
1
+ module Arel # :nodoc: all
2
+ module Nodes
3
+ class Settings < Arel::Nodes::Unary
4
+ def initialize(expr)
5
+ raise ArgumentError, 'Settings must be a Hash' unless expr.is_a?(Hash)
6
+
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,54 @@
1
+ require 'arel/visitors/to_sql'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class Clickhouse < ::Arel::Visitors::ToSql
6
+
7
+ def aggregate(name, o, collector)
8
+ # replacing function name for materialized view
9
+ if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation&.is_view
10
+ super("#{name.downcase}Merge", o, collector)
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def visit_Arel_Table o, collector
17
+ collector = super
18
+ collector << ' FINAL ' if o.final
19
+ collector
20
+ end
21
+
22
+ def visit_Arel_Nodes_SelectOptions(o, collector)
23
+ maybe_visit o.settings, super
24
+ end
25
+
26
+ def visit_Arel_Nodes_Settings(o, collector)
27
+ return collector if o.expr.empty?
28
+
29
+ collector << "SETTINGS "
30
+ o.expr.each_with_index do |(key, value), i|
31
+ collector << ", " if i > 0
32
+ collector << key.to_s.gsub(/\W+/, "")
33
+ collector << " = "
34
+ collector << sanitize_as_setting_value(value)
35
+ end
36
+ collector
37
+ end
38
+
39
+ def sanitize_as_setting_value(value)
40
+ if value == :default
41
+ 'DEFAULT'
42
+ else
43
+ quote(value)
44
+ end
45
+ end
46
+
47
+ def sanitize_as_setting_name(value)
48
+ return value if Arel::Nodes::SqlLiteral === value
49
+ @connection.sanitize_as_setting_name(value)
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -28,20 +28,32 @@ module ClickhouseActiverecord
28
28
  end
29
29
 
30
30
  def all_versions
31
- from("#{table_name} FINAL").where(active: 1).order(:version).pluck(:version)
31
+ final.where(active: 1).order(:version).pluck(:version)
32
32
  end
33
33
  end
34
34
  end
35
35
 
36
36
  class InternalMetadata < ::ActiveRecord::InternalMetadata
37
37
  class << self
38
+
39
+ def []=(key, value)
40
+ row = final.find_by(key: key)
41
+ if row.nil? || row.value != value
42
+ create!(key: key, value: value)
43
+ end
44
+ end
45
+
46
+ def [](key)
47
+ final.where(key: key).pluck(:value).first
48
+ end
49
+
38
50
  def create_table
39
51
  return if table_exists?
40
52
 
41
53
  key_options = connection.internal_string_options_for_primary_key
42
54
  table_options = {
43
55
  id: false,
44
- options: connection.adapter_name.downcase == 'clickhouse' ? 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)' : '',
56
+ options: connection.adapter_name.downcase == 'clickhouse' ? 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key' : '',
45
57
  if_not_exists: true
46
58
  }
47
59
  full_config = connection.instance_variable_get(:@full_config) || {}
@@ -122,5 +134,13 @@ module ClickhouseActiverecord
122
134
  super
123
135
  end
124
136
  end
137
+
138
+ private
139
+
140
+ def record_environment
141
+ return if down?
142
+ ClickhouseActiverecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
143
+ end
144
+
125
145
  end
126
146
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '0.5.14'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -4,6 +4,10 @@ require 'active_record/connection_adapters/clickhouse_adapter'
4
4
 
5
5
  require 'core_extensions/active_record/relation'
6
6
 
7
+ require 'core_extensions/arel/nodes/select_statement'
8
+ require 'core_extensions/arel/select_manager'
9
+ require 'core_extensions/arel/table'
10
+
7
11
  require_relative '../core_extensions/active_record/migration/command_recorder'
8
12
  ActiveRecord::Migration::CommandRecorder.include CoreExtensions::ActiveRecord::Migration::CommandRecorder
9
13
 
@@ -18,5 +22,9 @@ end
18
22
  module ClickhouseActiverecord
19
23
  def self.load
20
24
  ActiveRecord::Relation.prepend(CoreExtensions::ActiveRecord::Relation)
25
+
26
+ Arel::Nodes::SelectStatement.prepend(CoreExtensions::Arel::Nodes::SelectStatement)
27
+ Arel::SelectManager.prepend(CoreExtensions::Arel::SelectManager)
28
+ Arel::Table.prepend(CoreExtensions::Arel::Table)
21
29
  end
22
30
  end
@@ -10,6 +10,35 @@ module CoreExtensions
10
10
  self.order_values = (column_names & %w[date created_at]).map { |c| arel_table[c].desc }
11
11
  self
12
12
  end
13
+
14
+ # @param [Hash] opts
15
+ def settings(**opts)
16
+ check_command('SETTINGS')
17
+ @values[:settings] = (@values[:settings] || {}).merge opts
18
+ self
19
+ end
20
+
21
+ # @param [Boolean] final
22
+ def final(final = true)
23
+ check_command('FINAL')
24
+ @table = @table.dup
25
+ @table.final = final
26
+ self
27
+ end
28
+
29
+ private
30
+
31
+ def check_command(cmd)
32
+ raise ::ActiveRecord::ActiveRecordError, cmd + ' is a ClickHouse specific query clause' unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
33
+ end
34
+
35
+ def build_arel(aliases = nil)
36
+ arel = super
37
+
38
+ arel.settings(@values[:settings]) if @values[:settings].present?
39
+
40
+ arel
41
+ end
13
42
  end
14
43
  end
15
44
  end
@@ -0,0 +1,18 @@
1
+ module CoreExtensions
2
+ module Arel # :nodoc: all
3
+ module Nodes
4
+ module SelectStatement
5
+ attr_accessor :settings
6
+
7
+ def initialize
8
+ super
9
+ @settings = nil
10
+ end
11
+
12
+ def eql?(other)
13
+ super && settings == other.settings
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ module CoreExtensions
2
+ module Arel
3
+ module SelectManager
4
+
5
+ # @param [Hash] values
6
+ def settings(values)
7
+ @ast.settings = ::Arel::Nodes::Settings.new(values)
8
+ self
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,6 +1,8 @@
1
- module ClickhouseActiverecord
1
+ module CoreExtensions
2
2
  module Arel
3
- class Table < ::Arel::Table
3
+ module Table
4
+ attr_accessor :final
5
+
4
6
  def is_view
5
7
  type_caster.is_view
6
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clickhouse-activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.14
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-21 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5.2'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '1.15'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '1.15'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -121,9 +107,9 @@ files:
121
107
  - lib/active_record/connection_adapters/clickhouse/schema_definitions.rb
122
108
  - lib/active_record/connection_adapters/clickhouse/schema_statements.rb
123
109
  - lib/active_record/connection_adapters/clickhouse_adapter.rb
110
+ - lib/arel/nodes/settings.rb
111
+ - lib/arel/visitors/clickhouse.rb
124
112
  - lib/clickhouse-activerecord.rb
125
- - lib/clickhouse-activerecord/arel/table.rb
126
- - lib/clickhouse-activerecord/arel/visitors/to_sql.rb
127
113
  - lib/clickhouse-activerecord/migration.rb
128
114
  - lib/clickhouse-activerecord/railtie.rb
129
115
  - lib/clickhouse-activerecord/schema.rb
@@ -131,6 +117,9 @@ files:
131
117
  - lib/clickhouse-activerecord/tasks.rb
132
118
  - lib/clickhouse-activerecord/version.rb
133
119
  - lib/core_extensions/active_record/relation.rb
120
+ - lib/core_extensions/arel/nodes/select_statement.rb
121
+ - lib/core_extensions/arel/select_manager.rb
122
+ - lib/core_extensions/arel/table.rb
134
123
  - lib/generators/clickhouse_migration_generator.rb
135
124
  - lib/tasks/clickhouse.rake
136
125
  homepage: https://github.com/pnixx/clickhouse-activerecord
@@ -152,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
141
  - !ruby/object:Gem::Version
153
142
  version: '0'
154
143
  requirements: []
155
- rubygems_version: 3.0.4
144
+ rubygems_version: 3.1.6
156
145
  signing_key:
157
146
  specification_version: 4
158
147
  summary: ClickHouse ActiveRecord
@@ -1,20 +0,0 @@
1
- require 'arel/visitors/to_sql'
2
-
3
- module ClickhouseActiverecord
4
- module Arel
5
- module Visitors
6
- class ToSql < ::Arel::Visitors::ToSql
7
-
8
- def aggregate(name, o, collector)
9
- # replacing function name for materialized view
10
- if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation&.is_view
11
- super("#{name.downcase}Merge", o, collector)
12
- else
13
- super
14
- end
15
- end
16
-
17
- end
18
- end
19
- end
20
- end