clickhouse-activerecord 0.5.14 → 0.6.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: 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