Class: ClickhouseRuby::ActiveRecord::ConnectionAdapter

Inherits:
ActiveRecord::ConnectionAdapters::AbstractAdapter
  • Object
show all
Includes:
SchemaStatements
Defined in:
lib/clickhouse_ruby/active_record/connection_adapter.rb

Overview

Note:

ClickHouse has significant differences from traditional RDBMS:

  • No transaction support (commits are immediate)

  • DELETE uses ALTER TABLE … DELETE WHERE syntax

  • UPDATE uses ALTER TABLE … UPDATE … WHERE syntax

  • No foreign key constraints

  • No savepoints

ClickHouse database connection adapter for ActiveRecord

This adapter allows Rails applications to use ClickHouse as a database backend through ActiveRecord’s standard interface.

Examples:

database.yml configuration

development:
  adapter: clickhouse
  host: localhost
  port: 8123
  database: analytics
  username: default
  password: ''
  ssl: false
  ssl_verify: true

Model usage

class Event < ApplicationRecord
  self.table_name = 'events'
end

Event.where(user_id: 123).count
Event.insert_all(records)
Event.where(status: 'old').delete_all  # Raises on error!

Constant Summary collapse

ADAPTER_NAME =
'Clickhouse'
NATIVE_DATABASE_TYPES =

Native database types mapping for ClickHouse Used by migrations and schema definitions

{
  primary_key: 'UInt64',
  string: { name: 'String' },
  text: { name: 'String' },
  integer: { name: 'Int32' },
  bigint: { name: 'Int64' },
  float: { name: 'Float32' },
  decimal: { name: 'Decimal', precision: 10, scale: 0 },
  datetime: { name: 'DateTime' },
  timestamp: { name: 'DateTime64', precision: 3 },
  time: { name: 'DateTime' },
  date: { name: 'Date' },
  binary: { name: 'String' },
  boolean: { name: 'UInt8' },
  uuid: { name: 'UUID' },
  json: { name: 'String' }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SchemaStatements

#add_column, #add_index, #change_column, #column_exists?, #columns, #create_database, #create_table, #current_database, #databases, #drop_database, #drop_table, #index_exists?, #indexes, #primary_keys, #remove_column, #remove_index, #rename_column, #rename_table, #table_exists?, #tables, #truncate_table, #view_exists?, #views

Constructor Details

#initialize(connection, logger = nil, connection_options = nil, config = {}) ⇒ ConnectionAdapter

Initialize a new ConnectionAdapter

Parameters:

  • connection (Object, nil)

    existing connection

  • logger (Logger) (defaults to: nil)

    Rails logger

  • connection_options (Array) (defaults to: nil)

    connection options

  • config (Hash) (defaults to: {})

    database configuration



113
114
115
116
117
118
119
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 113

def initialize(connection, logger = nil, connection_options = nil, config = {})
  @config = config.symbolize_keys
  @chruby_client = nil
  @connection_parameters = nil

  super(connection, logger, config)
end

Class Method Details

.new_client(config) ⇒ ConnectionAdapter

Creates a new database connection Called by ActiveRecord’s connection handler

Parameters:

  • connection (Object, nil)

    existing connection (unused)

  • logger (Logger)

    Rails logger

  • connection_options (Hash)

    unused

  • config (Hash)

    database configuration

Returns:



75
76
77
78
79
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 75

def new_client(config)
  chruby_config = build_chruby_config(config)
  chruby_config.validate!
  ClickhouseRuby::Client.new(chruby_config)
end

Instance Method Details

#active?Boolean

Check if the connection is active

Returns:

  • (Boolean)

    true if connected and responding



142
143
144
145
146
147
148
149
150
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 142

def active?
  return false unless @chruby_client

  # Ping ClickHouse to verify connection
  execute_internal('SELECT 1')
  true
rescue ClickhouseRuby::Error
  false
end

#adapter_nameString

Returns the adapter name

Returns:

  • (String)

    ‘Clickhouse’



124
125
126
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 124

def adapter_name
  ADAPTER_NAME
end

#arel_visitorArelVisitor

Returns the Arel visitor for ClickHouse SQL generation

Returns:



498
499
500
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 498

def arel_visitor
  @arel_visitor ||= ArelVisitor.new(self)
end

#begin_db_transactionvoid

This method returns an undefined value.

Begin a transaction (no-op for ClickHouse) ClickHouse doesn’t support multi-statement transactions



440
441
442
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 440

def begin_db_transaction
  # No-op: ClickHouse doesn't support transactions
end

#commit_db_transactionvoid

This method returns an undefined value.

Commit a transaction (no-op for ClickHouse) All statements are auto-committed in ClickHouse



448
449
450
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 448

def commit_db_transaction
  # No-op: ClickHouse doesn't support transactions
end

#connectvoid

This method returns an undefined value.

Establish connection to ClickHouse



187
188
189
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 187

def connect
  @chruby_client = self.class.new_client(@config)
end

#connected?Boolean

Check if connected to the database

Returns:

  • (Boolean)

    true if we have a client instance



155
156
157
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 155

def connected?
  !@chruby_client.nil?
end

#disconnect!void

This method returns an undefined value.

Disconnect from the database



162
163
164
165
166
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 162

def disconnect!
  super
  @chruby_client&.close if @chruby_client.respond_to?(:close)
  @chruby_client = nil
end

#exec_delete(sql, name = nil, binds = []) ⇒ Integer

Execute a DELETE statement CRITICAL: This method MUST raise on errors (Issue #230)

ClickHouse DELETE syntax: ALTER TABLE table DELETE WHERE condition This method handles the conversion automatically via Arel visitor

Parameters:

  • sql (String)

    the DELETE SQL (converted to ALTER TABLE … DELETE)

  • name (String) (defaults to: nil)

    query name for logging

  • binds (Array) (defaults to: [])

    bind values

Returns:

  • (Integer)

    number of affected rows (estimated, ClickHouse doesn’t return exact count)

Raises:



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 358

def exec_delete(sql, name = nil, binds = [])
  ensure_connected!

  # The Arel visitor should have already converted this to
  # ALTER TABLE ... DELETE WHERE syntax
  # But if it's standard DELETE, convert it here
  clickhouse_sql = convert_delete_to_alter(sql)

  log(clickhouse_sql, name || 'DELETE') do
    result = execute_internal(clickhouse_sql)
    # CRITICAL: Raise on any error
    raise_if_error!(result)

    # ClickHouse doesn't return affected row count for mutations
    # Return 0 as a safe default, but the operation succeeded
    0
  end
rescue ClickhouseRuby::Error => e
  # CRITICAL: Always propagate errors, never silently fail
  raise_query_error(e, sql)
rescue StandardError => e
  raise ClickhouseRuby::QueryError.new(
    "DELETE failed: #{e.message}",
    sql: sql,
    original_error: e
  )
end

#exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) ⇒ Object

Execute an INSERT statement For bulk inserts, use insert_all which is more efficient

Parameters:

  • sql (String)

    the INSERT SQL

  • name (String) (defaults to: nil)

    query name for logging

  • pk (String, nil) (defaults to: nil)

    primary key column

  • id_value (Object, nil)

    id value

  • sequence_name (String, nil) (defaults to: nil)

    sequence name (unused)

  • binds (Array) (defaults to: [])

    bind values

Returns:

  • (Object)

    the id value

Raises:



340
341
342
343
344
345
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 340

def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
  execute(sql, name)
  # ClickHouse doesn't return inserted IDs
  # Return nil as we can't get the last insert ID
  nil
end

#exec_query(sql, name = 'SQL', binds = [], prepare: false) ⇒ ClickhouseRuby::Result

Execute a raw query, returning results

Parameters:

  • sql (String)

    the SQL query

  • name (String) (defaults to: 'SQL')

    query name for logging

  • binds (Array) (defaults to: [])

    bind values

  • prepare (Boolean) (defaults to: false)

    whether to prepare (ignored, ClickHouse doesn’t support)

Returns:



428
429
430
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 428

def exec_query(sql, name = 'SQL', binds = [], prepare: false)
  execute(sql, name)
end

#exec_rollback_db_transactionvoid

This method returns an undefined value.

Rollback a transaction (no-op for ClickHouse) ClickHouse doesn’t support rollback



456
457
458
459
460
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 456

def exec_rollback_db_transaction
  # No-op: ClickHouse doesn't support transactions
  # Log a warning since rollback was requested but cannot be performed
  @logger&.warn('ClickHouse does not support transaction rollback')
end

#exec_update(sql, name = nil, binds = []) ⇒ Integer

Execute an UPDATE statement CRITICAL: This method MUST raise on errors

ClickHouse UPDATE syntax: ALTER TABLE table UPDATE col = val WHERE condition This method handles the conversion automatically via Arel visitor

Parameters:

  • sql (String)

    the UPDATE SQL (converted to ALTER TABLE … UPDATE)

  • name (String) (defaults to: nil)

    query name for logging

  • binds (Array) (defaults to: [])

    bind values

Returns:

  • (Integer)

    number of affected rows (estimated)

Raises:



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 397

def exec_update(sql, name = nil, binds = [])
  ensure_connected!

  # The Arel visitor should have already converted this to
  # ALTER TABLE ... UPDATE ... WHERE syntax
  clickhouse_sql = convert_update_to_alter(sql)

  log(clickhouse_sql, name || 'UPDATE') do
    result = execute_internal(clickhouse_sql)
    raise_if_error!(result)

    # ClickHouse doesn't return affected row count for mutations
    0
  end
rescue ClickhouseRuby::Error => e
  raise_query_error(e, sql)
rescue StandardError => e
  raise ClickhouseRuby::QueryError.new(
    "UPDATE failed: #{e.message}",
    sql: sql,
    original_error: e
  )
end

#execute(sql, name = nil) ⇒ ClickhouseRuby::Result

Execute a SQL query CRITICAL: This method MUST raise on errors, never silently fail

Parameters:

  • sql (String)

    the SQL query

  • name (String) (defaults to: nil)

    query name for logging

Returns:

Raises:



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 307

def execute(sql, name = nil)
  ensure_connected!

  log(sql, name) do
    result = execute_internal(sql)
    # CRITICAL: Check for errors and raise them
    # ClickHouse may return 200 OK with error in body
    raise_if_error!(result)
    result
  end
rescue ClickhouseRuby::Error => e
  # Re-raise ClickhouseRuby errors with the SQL context
  raise_query_error(e, sql)
rescue StandardError => e
  # Wrap unexpected errors
  raise ClickhouseRuby::QueryError.new(
    "Query execution failed: #{e.message}",
    sql: sql,
    original_error: e
  )
end

#initialize_type_map(m = type_map) ⇒ void

This method returns an undefined value.

Initialize the type map with ClickHouse types

Parameters:

  • m (ActiveRecord::Type::TypeMap) (defaults to: type_map)

    the type map to populate



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 510

def initialize_type_map(m = type_map)
  # Register standard types
  register_class_with_limit m, %r{^String}i, ::ActiveRecord::Type::String
  register_class_with_limit m, %r{^FixedString}i, ::ActiveRecord::Type::String

  # Integer types
  m.register_type %r{^Int8}i, ::ActiveRecord::Type::Integer.new(limit: 1)
  m.register_type %r{^Int16}i, ::ActiveRecord::Type::Integer.new(limit: 2)
  m.register_type %r{^Int32}i, ::ActiveRecord::Type::Integer.new(limit: 4)
  m.register_type %r{^Int64}i, ::ActiveRecord::Type::Integer.new(limit: 8)
  m.register_type %r{^UInt8}i, ::ActiveRecord::Type::Integer.new(limit: 1)
  m.register_type %r{^UInt16}i, ::ActiveRecord::Type::Integer.new(limit: 2)
  m.register_type %r{^UInt32}i, ::ActiveRecord::Type::Integer.new(limit: 4)
  m.register_type %r{^UInt64}i, ::ActiveRecord::Type::Integer.new(limit: 8)

  # Float types
  m.register_type %r{^Float32}i, ::ActiveRecord::Type::Float.new
  m.register_type %r{^Float64}i, ::ActiveRecord::Type::Float.new

  # Decimal types
  m.register_type %r{^Decimal}i, ::ActiveRecord::Type::Decimal.new

  # Date/Time types
  m.register_type %r{^Date$}i, ::ActiveRecord::Type::Date.new
  m.register_type %r{^DateTime}i, ::ActiveRecord::Type::DateTime.new
  m.register_type %r{^DateTime64}i, ::ActiveRecord::Type::DateTime.new

  # Boolean (UInt8 with 0/1)
  m.register_type %r{^Bool}i, ::ActiveRecord::Type::Boolean.new

  # UUID
  m.register_type %r{^UUID}i, ::ActiveRecord::Type::String.new

  # Nullable wrapper - extract inner type
  m.register_type %r{^Nullable\((.+)\)}i do |sql_type|
    inner_type = sql_type.match(%r{^Nullable\((.+)\)}i)[1]
    lookup_cast_type(inner_type)
  end

  # Array types
  m.register_type %r{^Array\(}i, ::ActiveRecord::Type::String.new

  # Map types
  m.register_type %r{^Map\(}i, ::ActiveRecord::Type::String.new

  # Tuple types
  m.register_type %r{^Tuple\(}i, ::ActiveRecord::Type::String.new

  # Enum types (treated as strings)
  m.register_type %r{^Enum}i, ::ActiveRecord::Type::String.new

  # LowCardinality wrapper
  m.register_type %r{^LowCardinality\((.+)\)}i do |sql_type|
    inner_type = sql_type.match(%r{^LowCardinality\((.+)\)}i)[1]
    lookup_cast_type(inner_type)
  end
end

#native_database_typesHash

Returns native database types

Returns:

  • (Hash)

    type mapping



131
132
133
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 131

def native_database_types
  NATIVE_DATABASE_TYPES
end

#quote_column_name(name) ⇒ String

Quote a column name for ClickHouse ClickHouse uses backticks or double quotes for identifiers

Parameters:

  • name (String, Symbol)

    the column name

Returns:

  • (String)

    the quoted column name



471
472
473
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 471

def quote_column_name(name)
  "`#{name.to_s.gsub('`', '``')}`"
end

#quote_string(string) ⇒ String

Quote a string value for ClickHouse

Parameters:

  • string (String)

    the string to quote

Returns:

  • (String)

    the quoted string



487
488
489
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 487

def quote_string(string)
  string.gsub("\\", "\\\\\\\\").gsub("'", "\\\\'")
end

#quote_table_name(name) ⇒ String

Quote a table name for ClickHouse

Parameters:

  • name (String, Symbol)

    the table name

Returns:

  • (String)

    the quoted table name



479
480
481
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 479

def quote_table_name(name)
  "`#{name.to_s.gsub('`', '``')}`"
end

#reconnect!void

This method returns an undefined value.

Reconnect to the database



171
172
173
174
175
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 171

def reconnect!
  super
  disconnect!
  connect
end

#reset!void

This method returns an undefined value.

Clear the connection (called when returning connection to pool)



180
181
182
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 180

def reset!
  reconnect!
end

#supports_bulk_alter?Boolean

ClickHouse doesn’t support bulk alter

Returns:

  • (Boolean)

    false



283
284
285
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 283

def supports_bulk_alter?
  false
end

#supports_check_constraints?Boolean

ClickHouse doesn’t support check constraints in the traditional sense

Returns:

  • (Boolean)

    false



234
235
236
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 234

def supports_check_constraints?
  false
end

#supports_comments?Boolean

ClickHouse doesn’t support standard comments on columns

Returns:

  • (Boolean)

    false



276
277
278
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 276

def supports_comments?
  false
end

#supports_datetime_with_precision?Boolean

ClickHouse supports datetime with precision (DateTime64)

Returns:

  • (Boolean)

    true



262
263
264
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 262

def supports_datetime_with_precision?
  true
end

#supports_ddl_transactions?Boolean

ClickHouse doesn’t support DDL transactions

Returns:

  • (Boolean)

    false



199
200
201
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 199

def supports_ddl_transactions?
  false
end

#supports_explain?Boolean

ClickHouse supports EXPLAIN

Returns:

  • (Boolean)

    true



290
291
292
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 290

def supports_explain?
  true
end

#supports_expression_index?Boolean

ClickHouse doesn’t support expression indexes

Returns:

  • (Boolean)

    false



248
249
250
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 248

def supports_expression_index?
  false
end

#supports_foreign_keys?Boolean

ClickHouse doesn’t support foreign keys

Returns:

  • (Boolean)

    false



227
228
229
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 227

def supports_foreign_keys?
  false
end

#supports_insert_returning?Boolean

ClickHouse doesn’t support INSERT RETURNING

Returns:

  • (Boolean)

    false



220
221
222
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 220

def supports_insert_returning?
  false
end

#supports_json?Boolean

ClickHouse supports JSON type (as String with JSON functions)

Returns:

  • (Boolean)

    true



269
270
271
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 269

def supports_json?
  true
end

#supports_partial_index?Boolean

ClickHouse doesn’t support partial indexes

Returns:

  • (Boolean)

    false



241
242
243
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 241

def supports_partial_index?
  false
end

#supports_savepoints?Boolean

ClickHouse doesn’t support savepoints

Returns:

  • (Boolean)

    false



206
207
208
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 206

def supports_savepoints?
  false
end

#supports_transaction_isolation?Boolean

ClickHouse doesn’t support transaction isolation levels

Returns:

  • (Boolean)

    false



213
214
215
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 213

def supports_transaction_isolation?
  false
end

#supports_views?Boolean

ClickHouse doesn’t support standard views (has MATERIALIZED VIEWS)

Returns:

  • (Boolean)

    false



255
256
257
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 255

def supports_views?
  false
end