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)

    connection options

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

    database configuration



114
115
116
117
118
119
120
121
122
123
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 114

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

  super(connection, logger, config)

  # Extend ActiveRecord::Relation with our methods
  ::ActiveRecord::Relation.include(RelationExtensions)
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:



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

def new_client(config)
  clickhouse_config = build_clickhouse_config(config)
  clickhouse_config.validate!
  ClickhouseRuby::Client.new(clickhouse_config)
end

Instance Method Details

#active?Boolean

Check if the connection is active

Returns:

  • (Boolean)

    true if connected and responding



146
147
148
149
150
151
152
153
154
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 146

def active?
  return false unless @clickhouse_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’



128
129
130
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 128

def adapter_name
  ADAPTER_NAME
end

#arel_visitorArelVisitor

Returns the Arel visitor for ClickHouse SQL generation

Returns:



502
503
504
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 502

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



444
445
446
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 444

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



452
453
454
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 452

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

#connectvoid

This method returns an undefined value.

Establish connection to ClickHouse



191
192
193
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 191

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

#connected?Boolean

Check if connected to the database

Returns:

  • (Boolean)

    true if we have a client instance



159
160
161
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 159

def connected?
  !@clickhouse_client.nil?
end

#disconnect!void

This method returns an undefined value.

Disconnect from the database



166
167
168
169
170
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 166

def disconnect!
  super
  @clickhouse_client&.close if @clickhouse_client.respond_to?(:close)
  @clickhouse_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)

    bind values

Returns:

  • (Integer)

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

Raises:



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

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)

    primary key column

  • id_value (Object, nil)

    id value

  • sequence_name (String, nil)

    sequence name (unused)

  • binds (Array)

    bind values

Returns:

  • (Object)

    the id value

Raises:



344
345
346
347
348
349
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 344

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)

    bind values

  • prepare (Boolean) (defaults to: false)

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

Returns:



432
433
434
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 432

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



460
461
462
463
464
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 460

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)

    bind values

Returns:

  • (Integer)

    number of affected rows (estimated)

Raises:



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 401

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:



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 311

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



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
567
568
569
570
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 514

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

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

  # Float types
  m.register_type(/^Float32/i, ::ActiveRecord::Type::Float.new)
  m.register_type(/^Float64/i, ::ActiveRecord::Type::Float.new)

  # Decimal types
  m.register_type(/^Decimal/i, ::ActiveRecord::Type::Decimal.new)

  # Date/Time types
  m.register_type(/^Date$/i, ::ActiveRecord::Type::Date.new)
  m.register_type(/^DateTime/i, ::ActiveRecord::Type::DateTime.new)
  m.register_type(/^DateTime64/i, ::ActiveRecord::Type::DateTime.new)

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

  # UUID
  m.register_type(/^UUID/i, ::ActiveRecord::Type::String.new)

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

  # Array types
  m.register_type(/^Array\(/i, ::ActiveRecord::Type::String.new)

  # Map types
  m.register_type(/^Map\(/i, ::ActiveRecord::Type::String.new)

  # Tuple types
  m.register_type(/^Tuple\(/i, ::ActiveRecord::Type::String.new)

  # Enum types (treated as strings)
  m.register_type(/^Enum/i, ::ActiveRecord::Type::String.new)

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

#native_database_typesHash

Returns native database types

Returns:

  • (Hash)

    type mapping



135
136
137
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 135

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



475
476
477
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 475

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



491
492
493
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 491

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



483
484
485
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 483

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

#reconnect!void

This method returns an undefined value.

Reconnect to the database



175
176
177
178
179
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 175

def reconnect!
  super
  disconnect!
  connect
end

#reset!void

This method returns an undefined value.

Clear the connection (called when returning connection to pool)



184
185
186
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 184

def reset!
  reconnect!
end

#supports_bulk_alter?Boolean

ClickHouse doesn’t support bulk alter

Returns:

  • (Boolean)

    false



287
288
289
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 287

def supports_bulk_alter?
  false
end

#supports_check_constraints?Boolean

ClickHouse doesn’t support check constraints in the traditional sense

Returns:

  • (Boolean)

    false



238
239
240
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 238

def supports_check_constraints?
  false
end

#supports_comments?Boolean

ClickHouse doesn’t support standard comments on columns

Returns:

  • (Boolean)

    false



280
281
282
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 280

def supports_comments?
  false
end

#supports_datetime_with_precision?Boolean

ClickHouse supports datetime with precision (DateTime64)

Returns:

  • (Boolean)

    true



266
267
268
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 266

def supports_datetime_with_precision?
  true
end

#supports_ddl_transactions?Boolean

ClickHouse doesn’t support DDL transactions

Returns:

  • (Boolean)

    false



203
204
205
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 203

def supports_ddl_transactions?
  false
end

#supports_explain?Boolean

ClickHouse supports EXPLAIN

Returns:

  • (Boolean)

    true



294
295
296
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 294

def supports_explain?
  true
end

#supports_expression_index?Boolean

ClickHouse doesn’t support expression indexes

Returns:

  • (Boolean)

    false



252
253
254
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 252

def supports_expression_index?
  false
end

#supports_foreign_keys?Boolean

ClickHouse doesn’t support foreign keys

Returns:

  • (Boolean)

    false



231
232
233
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 231

def supports_foreign_keys?
  false
end

#supports_insert_returning?Boolean

ClickHouse doesn’t support INSERT RETURNING

Returns:

  • (Boolean)

    false



224
225
226
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 224

def supports_insert_returning?
  false
end

#supports_json?Boolean

ClickHouse supports JSON type (as String with JSON functions)

Returns:

  • (Boolean)

    true



273
274
275
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 273

def supports_json?
  true
end

#supports_partial_index?Boolean

ClickHouse doesn’t support partial indexes

Returns:

  • (Boolean)

    false



245
246
247
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 245

def supports_partial_index?
  false
end

#supports_savepoints?Boolean

ClickHouse doesn’t support savepoints

Returns:

  • (Boolean)

    false



210
211
212
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 210

def supports_savepoints?
  false
end

#supports_transaction_isolation?Boolean

ClickHouse doesn’t support transaction isolation levels

Returns:

  • (Boolean)

    false



217
218
219
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 217

def supports_transaction_isolation?
  false
end

#supports_views?Boolean

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

Returns:

  • (Boolean)

    false



259
260
261
# File 'lib/clickhouse_ruby/active_record/connection_adapter.rb', line 259

def supports_views?
  false
end