diff --git a/README.md b/README.md index f957e653..2c665031 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Clickhouse::Activerecord -A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2. +A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 7.1. Support ClickHouse version from 22.0 LTS. ## Installation @@ -50,41 +50,31 @@ class ActionView < ActiveRecord::Base end ``` -## Usage in Rails 5 +## Usage in Rails Add your `database.yml` connection information with postfix `_clickhouse` for you environment: ```yml -development_clickhouse: +development: adapter: clickhouse database: database ``` -Add to your model: +Your model example: ```ruby class Action < ActiveRecord::Base - establish_connection "#{Rails.env}_clickhouse".to_sym end ``` For materialized view model add: ```ruby class ActionView < ActiveRecord::Base - establish_connection "#{Rails.env}_clickhouse".to_sym self.is_view = true end ``` -Or global connection: - -```yml -development: - adapter: clickhouse - database: database -``` - -## Usage in Rails 6 with second database +## Usage in Rails with second database Add your `database.yml` connection information for you environment: @@ -102,31 +92,31 @@ Connection [Multiple Databases with Active Record](https://guides.rubyonrails.or ```ruby class Action < ActiveRecord::Base - connects_to database: { writing: :clickhouse, reading: :clickhouse } + establish_connection :clickhouse end ``` ### Rake tasks -**Note!** For Rails 6 you can use default rake tasks if you configure `migrations_paths` in your `database.yml`, for example: `rake db:migrate` - Create / drop / purge / reset database: - $ rake clickhouse:create - $ rake clickhouse:drop - $ rake clickhouse:purge - $ rake clickhouse:reset + $ rake db:create + $ rake db:drop + $ rake db:purge + $ rake db:reset -Prepare system tables for rails: +Or with multiple databases: - $ rake clickhouse:prepare_schema_migration_table - $ rake clickhouse:prepare_internal_metadata_table + $ rake db:create:clickhouse + $ rake db:drop:clickhouse + $ rake db:purge:clickhouse + $ rake db:reset:clickhouse Migration: $ rails g clickhouse_migration MIGRATION_NAME COLUMNS - $ rake clickhouse:migrate - $ rake clickhouse:rollback + $ rake db:migrate + $ rake db:rollback ### Dump / Load for multiple using databases @@ -195,20 +185,20 @@ User.joins(:actions).using(:group_id) Integer types are unsigned by default. Specify signed values with `:unsigned => false`. The default integer is `UInt32` -| Type (bit size) | Range | :limit (byte size) | -| :--- | :----: | ---: | -| Int8 | -128 to 127 | 1 | -| Int16 | -32768 to 32767 | 2 | -| Int32 | -2147483648 to 2,147,483,647 | 3,4 | -| Int64 | -9223372036854775808 to 9223372036854775807] | 5,6,7,8 | -| Int128 | ... | 9 - 15 | -| Int256 | ... | 16+ | -| UInt8 | 0 to 255 | 1 | -| UInt16 | 0 to 65,535 | 2 | -| UInt32 | 0 to 4,294,967,295 | 3,4 | -| UInt64 | 0 to 18446744073709551615 | 5,6,7,8 | -| UInt256 | 0 to ... | 8+ | -| Array | ... | ... | +| Type (bit size) | Range | :limit (byte size) | +|:----------------|:--------------------------------------------:|-------------------:| +| Int8 | -128 to 127 | 1 | +| Int16 | -32768 to 32767 | 2 | +| Int32 | -2147483648 to 2,147,483,647 | 3,4 | +| Int64 | -9223372036854775808 to 9223372036854775807] | 5,6,7,8 | +| Int128 | ... | 9 - 15 | +| Int256 | ... | 16+ | +| UInt8 | 0 to 255 | 1 | +| UInt16 | 0 to 65,535 | 2 | +| UInt32 | 0 to 4,294,967,295 | 3,4 | +| UInt64 | 0 to 18446744073709551615 | 5,6,7,8 | +| UInt256 | 0 to ... | 8+ | +| Array | ... | ... | Example: diff --git a/clickhouse-activerecord.gemspec b/clickhouse-activerecord.gemspec index 58711d24..57fda949 100644 --- a/clickhouse-activerecord.gemspec +++ b/clickhouse-activerecord.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_runtime_dependency 'bundler', '>= 1.13.4' - spec.add_runtime_dependency 'activerecord', '>= 5.2', '< 7' + spec.add_runtime_dependency 'activerecord', '>= 7.1' spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rspec', '~> 3.4' diff --git a/lib/active_record/connection_adapters/clickhouse/schema_statements.rb b/lib/active_record/connection_adapters/clickhouse/schema_statements.rb index 2fd2ff39..0d649362 100644 --- a/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +++ b/lib/active_record/connection_adapters/clickhouse/schema_statements.rb @@ -10,13 +10,13 @@ def execute(sql, name = nil, settings: {}) do_execute(sql, name, settings: settings) end - def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil) + def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil, returning: nil) new_sql = sql.dup.sub(/ (DEFAULT )?VALUES/, " VALUES") do_execute(new_sql, name, format: nil) true end - def exec_query(sql, name = nil, binds = [], prepare: false) + def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: false) result = do_execute(sql, name) ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'], result['meta'].map { |m| [m['name'], type_map.lookup(m['type'])] }.to_h) rescue ActiveRecord::ActiveRecordError => e @@ -137,7 +137,7 @@ def create_table_definition(table_name, **options) Clickhouse::TableDefinition.new(self, table_name, **options) end - def new_column_from_field(table_name, field) + def new_column_from_field(table_name, field, _definitions) sql_type = field[1] type_metadata = fetch_type_metadata(sql_type) default = field[3] diff --git a/lib/active_record/connection_adapters/clickhouse_adapter.rb b/lib/active_record/connection_adapters/clickhouse_adapter.rb index 48186107..607c87f2 100644 --- a/lib/active_record/connection_adapters/clickhouse_adapter.rb +++ b/lib/active_record/connection_adapters/clickhouse_adapter.rb @@ -136,7 +136,11 @@ def initialize(logger, connection_parameters, config, full_config) # Support SchemaMigration from v5.2.2 to v6+ def schema_migration # :nodoc: - ClickhouseActiverecord::SchemaMigration + ClickhouseActiverecord::SchemaMigration.new(self) + end + + def internal_metadata # :nodoc: + ClickhouseActiverecord::InternalMetadata.new(self) end def migrations_paths @@ -144,7 +148,7 @@ def migrations_paths end def migration_context # :nodoc: - ClickhouseActiverecord::MigrationContext.new(migrations_paths, schema_migration) + ClickhouseActiverecord::MigrationContext.new(migrations_paths, schema_migration, internal_metadata) end def arel_visitor # :nodoc: @@ -159,66 +163,73 @@ def valid_type?(type) !native_database_types[type].nil? end - def extract_limit(sql_type) # :nodoc: - case sql_type - when /(Nullable)?\(?String\)?/ - super('String') - when /(Nullable)?\(?U?Int8\)?/ - 1 - when /(Nullable)?\(?U?Int16\)?/ - 2 - when /(Nullable)?\(?U?Int32\)?/ - nil - when /(Nullable)?\(?U?Int64\)?/ - 8 - else - super + class << self + def extract_limit(sql_type) # :nodoc: + case sql_type + when /(Nullable)?\(?String\)?/ + super('String') + when /(Nullable)?\(?U?Int8\)?/ + 1 + when /(Nullable)?\(?U?Int16\)?/ + 2 + when /(Nullable)?\(?U?Int32\)?/ + nil + when /(Nullable)?\(?U?Int64\)?/ + 8 + else + super + end end - end - # `extract_scale` and `extract_precision` are the same as in the Rails abstract base class, - # except this permits a space after the comma + # `extract_scale` and `extract_precision` are the same as in the Rails abstract base class, + # except this permits a space after the comma - def extract_scale(sql_type) - case sql_type - when /\((\d+)\)/ then 0 - when /\((\d+)(,\s?(\d+))\)/ then $3.to_i + def extract_scale(sql_type) + case sql_type + when /\((\d+)\)/ then 0 + when /\((\d+)(,\s?(\d+))\)/ then $3.to_i + end end - end - def extract_precision(sql_type) - $1.to_i if sql_type =~ /\((\d+)(,\s?\d+)?\)/ - end - - def initialize_type_map(m) # :nodoc: - super - register_class_with_limit m, %r(String), Type::String - register_class_with_limit m, 'Date', Clickhouse::OID::Date - register_class_with_precision m, %r(datetime)i, Clickhouse::OID::DateTime - - register_class_with_limit m, %r(Int8), Type::Integer - register_class_with_limit m, %r(Int16), Type::Integer - register_class_with_limit m, %r(Int32), Type::Integer - register_class_with_limit m, %r(Int64), Type::Integer - register_class_with_limit m, %r(Int128), Type::Integer - register_class_with_limit m, %r(Int256), Type::Integer - - register_class_with_limit m, %r(UInt8), Type::UnsignedInteger - register_class_with_limit m, %r(UInt16), Type::UnsignedInteger - register_class_with_limit m, %r(UInt32), Type::UnsignedInteger - register_class_with_limit m, %r(UInt64), Type::UnsignedInteger - #register_class_with_limit m, %r(UInt128), Type::UnsignedInteger #not implemnted in clickhouse - register_class_with_limit m, %r(UInt256), Type::UnsignedInteger - # register_class_with_limit m, %r(Array), Clickhouse::OID::Array - m.register_type(%r(Array)) do |sql_type| - Clickhouse::OID::Array.new(sql_type) + def extract_precision(sql_type) + $1.to_i if sql_type =~ /\((\d+)(,\s?\d+)?\)/ end + + def initialize_type_map(m) # :nodoc: + super + register_class_with_limit m, %r(String), Type::String + register_class_with_limit m, 'Date', Clickhouse::OID::Date + register_class_with_precision m, %r(datetime)i, Clickhouse::OID::DateTime + + register_class_with_limit m, %r(Int8), Type::Integer + register_class_with_limit m, %r(Int16), Type::Integer + register_class_with_limit m, %r(Int32), Type::Integer + register_class_with_limit m, %r(Int64), Type::Integer + register_class_with_limit m, %r(Int128), Type::Integer + register_class_with_limit m, %r(Int256), Type::Integer + + register_class_with_limit m, %r(UInt8), Type::UnsignedInteger + register_class_with_limit m, %r(UInt16), Type::UnsignedInteger + register_class_with_limit m, %r(UInt32), Type::UnsignedInteger + register_class_with_limit m, %r(UInt64), Type::UnsignedInteger + #register_class_with_limit m, %r(UInt128), Type::UnsignedInteger #not implemnted in clickhouse + register_class_with_limit m, %r(UInt256), Type::UnsignedInteger + # register_class_with_limit m, %r(Array), Clickhouse::OID::Array + m.register_type(%r(Array)) do |sql_type| + Clickhouse::OID::Array.new(sql_type) + end + end + end + + # In Rails 7 used constant TYPE_MAP, we need redefine method + def type_map + @type_map ||= Type::TypeMap.new.tap { |m| ClickhouseAdapter.initialize_type_map(m) } end - def _quote(value) + def quote(value) case value when Array - '[' + value.map { |v| _quote(v) }.join(', ') + ']' + '[' + value.map { |v| quote(v) }.join(', ') + ']' else super end diff --git a/lib/arel/visitors/clickhouse.rb b/lib/arel/visitors/clickhouse.rb index c59b4b47..a63471a9 100644 --- a/lib/arel/visitors/clickhouse.rb +++ b/lib/arel/visitors/clickhouse.rb @@ -15,7 +15,7 @@ def aggregate(name, o, collector) def visit_Arel_Table o, collector collector = super - collector << ' FINAL ' if o.final + collector << ' FINAL' if o.final collector end diff --git a/lib/clickhouse-activerecord/migration.rb b/lib/clickhouse-activerecord/migration.rb index b208e848..330bdfaa 100644 --- a/lib/clickhouse-activerecord/migration.rb +++ b/lib/clickhouse-activerecord/migration.rb @@ -3,107 +3,96 @@ module ClickhouseActiverecord class SchemaMigration < ::ActiveRecord::SchemaMigration - class << self + def create_table + return if table_exists? - def create_table - return if table_exists? + version_options = connection.internal_string_options_for_primary_key + table_options = { + id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true + } + full_config = connection.instance_variable_get(:@full_config) || {} - version_options = connection.internal_string_options_for_primary_key - table_options = { - id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true - } - full_config = connection.instance_variable_get(:@full_config) || {} + if full_config[:distributed_service_tables] + table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)') - if full_config[:distributed_service_tables] - table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)') - - distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}" - end - - connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t| - t.string :version, **version_options - t.column :active, 'Int8', null: false, default: '1' - t.datetime :ver, null: false, default: -> { 'now()' } - end + distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}" end - def all_versions - final.where(active: 1).order(:version).pluck(:version) + connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t| + t.string :version, **version_options + t.column :active, 'Int8', null: false, default: '1' + t.datetime :ver, null: false, default: -> { 'now()' } end end - end - class InternalMetadata < ::ActiveRecord::InternalMetadata - class << self + def versions + table = arel_table.dup + table.final = true + sm = Arel::SelectManager.new(table) + sm.project(arel_table[primary_key]) + sm.order(arel_table[primary_key].asc) + sm.where([arel_table['active'].eq(1)]) - def []=(key, value) - row = final.find_by(key: key) - if row.nil? || row.value != value - create!(key: key, value: value) - end - end + connection.select_values(sm, "#{self.class} Load") + end - def [](key) - final.where(key: key).pluck(:value).first - end + def delete_version(version) + im = Arel::InsertManager.new(arel_table) + im.insert(arel_table[primary_key] => version.to_s, arel_table['active'] => 0) + connection.insert(im, "#{self.class} Create Rollback Version", primary_key, version) + end + end - def create_table - return if table_exists? + class InternalMetadata < ::ActiveRecord::InternalMetadata - key_options = connection.internal_string_options_for_primary_key - table_options = { - id: false, - options: connection.adapter_name.downcase == 'clickhouse' ? 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key' : '', - if_not_exists: true - } - full_config = connection.instance_variable_get(:@full_config) || {} + def create_table + return if table_exists? || !enabled? - if full_config[:distributed_service_tables] - table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)') + key_options = connection.internal_string_options_for_primary_key + table_options = { + id: false, + options: connection.adapter_name.downcase == 'clickhouse' ? 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key' : '', + if_not_exists: true + } + full_config = connection.instance_variable_get(:@full_config) || {} - distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}" - end + if full_config[:distributed_service_tables] + table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)') - connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t| - t.string :key, **key_options - t.string :value - t.timestamps - end + distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}" end - end - end - class MigrationContext < ::ActiveRecord::MigrationContext #:nodoc: - attr_reader :migrations_paths, :schema_migration - - def initialize(migrations_paths, schema_migration) - @migrations_paths = migrations_paths - @schema_migration = schema_migration + connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t| + t.string :key, **key_options + t.string :value + t.timestamps + end end - def up(target_version = nil) - selected_migrations = if block_given? - migrations.select { |m| yield m } - else - migrations - end + private - ClickhouseActiverecord::Migrator.new(:up, selected_migrations, schema_migration, target_version).migrate + def update_entry(key, new_value) + create_entry(key, new_value) end - def down(target_version = nil) - selected_migrations = if block_given? - migrations.select { |m| yield m } - else - migrations - end + def select_entry(key) + table = arel_table.dup + table.final = true + sm = Arel::SelectManager.new(table) + sm.project(Arel::Nodes::SqlLiteral.new("*")) + sm.where(table[primary_key].eq(Arel::Nodes::BindParam.new(key))) + sm.order(table[primary_key].asc) + sm.limit = 1 - ClickhouseActiverecord::Migrator.new(:down, selected_migrations, schema_migration, target_version).migrate + connection.select_all(sm, "#{self.class} Load").first end + end + + class MigrationContext < ::ActiveRecord::MigrationContext #:nodoc: def get_all_versions if schema_migration.table_exists? - schema_migration.all_versions.map(&:to_i) + schema_migration.versions.map(&:to_i) else [] end @@ -111,36 +100,4 @@ def get_all_versions end - class Migrator < ::ActiveRecord::Migrator - - def initialize(direction, migrations, schema_migration, target_version = nil) - @direction = direction - @target_version = target_version - @migrated_versions = nil - @migrations = migrations - @schema_migration = schema_migration - - validate(@migrations) - - @schema_migration.create_table - ClickhouseActiverecord::InternalMetadata.create_table - end - - def record_version_state_after_migrating(version) - if down? - migrated.delete(version) - @schema_migration.create!(version: version.to_s, active: 0) - else - super - end - end - - private - - def record_environment - return if down? - ClickhouseActiverecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment - end - - end end diff --git a/lib/clickhouse-activerecord/tasks.rb b/lib/clickhouse-activerecord/tasks.rb index f3e20033..cd1890b9 100644 --- a/lib/clickhouse-activerecord/tasks.rb +++ b/lib/clickhouse-activerecord/tasks.rb @@ -2,7 +2,6 @@ module ClickhouseActiverecord class Tasks - delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base def initialize(configuration) @@ -11,7 +10,7 @@ def initialize(configuration) def create establish_master_connection - connection.create_database @configuration["database"] + connection.create_database @configuration['database'] rescue ActiveRecord::StatementInvalid => e if e.cause.to_s.include?('already exists') raise ActiveRecord::DatabaseAlreadyExists @@ -22,7 +21,7 @@ def create def drop establish_master_connection - connection.drop_database @configuration["database"] + connection.drop_database @configuration['database'] end def purge diff --git a/lib/clickhouse-activerecord/version.rb b/lib/clickhouse-activerecord/version.rb index 901480fe..fd3b523d 100644 --- a/lib/clickhouse-activerecord/version.rb +++ b/lib/clickhouse-activerecord/version.rb @@ -1,3 +1,3 @@ module ClickhouseActiverecord - VERSION = '0.6.1' + VERSION = '1.0.0' end diff --git a/lib/core_extensions/active_record/relation.rb b/lib/core_extensions/active_record/relation.rb index 7a938d1d..35c7a50d 100644 --- a/lib/core_extensions/active_record/relation.rb +++ b/lib/core_extensions/active_record/relation.rb @@ -11,21 +11,65 @@ def reverse_order! self end + # Define settings in the SETTINGS clause of the SELECT query. The setting value is applied only to that query and is reset to the default or previous value after the query is executed. + # For example: + # + # users = User.settings(optimize_read_in_order: 1, cast_keep_nullable: 1).where(name: 'John') + # # SELECT users.* FROM users WHERE users.name = 'John' SETTINGS optimize_read_in_order = 1, cast_keep_nullable = 1 + # + # An ActiveRecord::ActiveRecordError will be raised if database not ClickHouse. # @param [Hash] opts def settings(**opts) + spawn.settings!(**opts) + end + + # @param [Hash] opts + def settings!(**opts) + assert_mutability! check_command('SETTINGS') @values[:settings] = (@values[:settings] || {}).merge opts self end + # When FINAL is specified, ClickHouse fully merges the data before returning the result and thus performs all data transformations that happen during merges for the given table engine. + # For example: + # + # users = User.final.all + # # SELECT users.* FROM users FINAL + # + # An ActiveRecord::ActiveRecordError will be raised if database not ClickHouse. # @param [Boolean] final def final(final = true) + spawn.final!(final) + end + + # @param [Boolean] final + def final!(final = true) + assert_mutability! check_command('FINAL') @table = @table.dup @table.final = final self end + # The USING clause specifies one or more columns to join, which establishes the equality of these columns. For example: + # + # users = User.joins(:joins).using(:event_name, :date) + # # SELECT users.* FROM users INNER JOIN joins USING event_name,date + # + # An ActiveRecord::ActiveRecordError will be raised if database not ClickHouse. + # @param [Array] opts + def using(*opts) + spawn.using!(*opts) + end + + # @param [Array] opts + def using!(*opts) + assert_mutability! + @values[:using] = opts + self + end + private def check_command(cmd) @@ -36,6 +80,7 @@ def build_arel(aliases = nil) arel = super arel.settings(@values[:settings]) if @values[:settings].present? + arel.using(@values[:using]) if @values[:using].present? arel end diff --git a/lib/core_extensions/arel/nodes/select_statement.rb b/lib/core_extensions/arel/nodes/select_statement.rb index d7f3807e..b8f26ab5 100644 --- a/lib/core_extensions/arel/nodes/select_statement.rb +++ b/lib/core_extensions/arel/nodes/select_statement.rb @@ -4,7 +4,7 @@ module Nodes module SelectStatement attr_accessor :settings - def initialize + def initialize(relation = nil) super @settings = nil end diff --git a/lib/tasks/clickhouse.rake b/lib/tasks/clickhouse.rake index 649ff348..181ac42c 100644 --- a/lib/tasks/clickhouse.rake +++ b/lib/tasks/clickhouse.rake @@ -1,86 +1,87 @@ # frozen_string_literal: true namespace :clickhouse do - task prepare_schema_migration_table: :environment do - ClickhouseActiverecord::SchemaMigration.create_table unless ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? + connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection + connection.schema_migration.create_table unless ENV['simple'] || ARGV.any? { |a| a.include?('--simple') } end task prepare_internal_metadata_table: :environment do - ClickhouseActiverecord::InternalMetadata.create_table unless ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? - end - - task load_config: :environment do - ENV['SCHEMA'] = "db/clickhouse_schema.rb" - ActiveRecord::Migrator.migrations_paths = ["db/migrate_clickhouse"] - ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse") + connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection + connection.internal_metadata.create_table unless ENV['simple'] || ARGV.any? { |a| a.include?('--simple') } end namespace :schema do - - # todo not testing + # TODO: not testing desc 'Load database schema' - task load: [:load_config, :prepare_internal_metadata_table] do |t, args| - simple = ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? ? '_simple' : nil + task load: %i[load_config prepare_internal_metadata_table] do + simple = ENV['simple'] || ARGV.any? { |a| a.include?('--simple') } ? '_simple' : nil ClickhouseActiverecord::SchemaMigration.drop_table - load("#{Rails.root}/db/clickhouse_schema#{simple}.rb") + load(Rails.root.join("db/clickhouse_schema#{simple}.rb")) end desc 'Dump database schema' - task dump: :environment do |t, args| - simple = ENV['simple'] || args[:simple] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? ? '_simple' : nil - filename = "#{Rails.root}/db/clickhouse_schema#{simple}.rb" + task dump: :environment do |_, args| + simple = ENV['simple'] || args[:simple] || ARGV.any? { |a| a.include?('--simple') } ? '_simple' : nil + filename = Rails.root.join("db/clickhouse_schema#{simple}.rb") File.open(filename, 'w:utf-8') do |file| - ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse") - ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file, ActiveRecord::Base, !!simple) + ActiveRecord::Base.establish_connection(:clickhouse) + ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file, ActiveRecord::Base, simple.present?) end end - end namespace :structure do desc 'Load database structure' - task load: [:load_config, 'db:check_protected_environments'] do - ClickhouseActiverecord::Tasks.new(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]).structure_load("#{Rails.root}/db/clickhouse_structure.sql") + task load: ['db:check_protected_environments'] do + config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse') + ClickhouseActiverecord::Tasks.new(config).structure_load(Rails.root.join('db/clickhouse_structure.sql')) end desc 'Dump database structure' - task dump: [:load_config, 'db:check_protected_environments'] do - ClickhouseActiverecord::Tasks.new(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]).structure_dump("#{Rails.root}/db/clickhouse_structure.sql") + task dump: ['db:check_protected_environments'] do + config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse') + ClickhouseActiverecord::Tasks.new(config).structure_dump(Rails.root.join('db/clickhouse_structure.sql')) end end desc 'Creates the database from DATABASE_URL or config/database.yml' - task create: [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.create(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]) + task create: [] do + config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse') + ActiveRecord::Tasks::DatabaseTasks.create(config) end desc 'Drops the database from DATABASE_URL or config/database.yml' - task drop: [:load_config, 'db:check_protected_environments'] do - ActiveRecord::Tasks::DatabaseTasks.drop(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]) + task drop: ['db:check_protected_environments'] do + config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse') + ActiveRecord::Tasks::DatabaseTasks.drop(config) end desc 'Empty the database from DATABASE_URL or config/database.yml' - task purge: [:load_config, 'db:check_protected_environments'] do - ActiveRecord::Tasks::DatabaseTasks.purge(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]) + task purge: ['db:check_protected_environments'] do + config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse') + ActiveRecord::Tasks::DatabaseTasks.purge(config) end # desc 'Resets your database using your migrations for the current environment' - task reset: :load_config do + task :reset do Rake::Task['clickhouse:purge'].execute Rake::Task['clickhouse:migrate'].execute end desc 'Migrate the clickhouse database' - task migrate: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do - Rake::Task['db:migrate'].execute + task migrate: %i[prepare_schema_migration_table prepare_internal_metadata_table] do + Rake::Task['db:migrate:clickhouse'].execute if File.exists? "#{Rails.root}/db/clickhouse_schema_simple.rb" Rake::Task['clickhouse:schema:dump'].execute(simple: true) end end desc 'Rollback the clickhouse database' - task rollback: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do - Rake::Task['db:rollback'].execute + task rollback: %i[prepare_schema_migration_table prepare_internal_metadata_table] do + Rake::Task['db:rollback:clickhouse'].execute + if File.exists? "#{Rails.root}/db/clickhouse_schema_simple.rb" + Rake::Task['clickhouse:schema:dump'].execute(simple: true) + end end end diff --git a/spec/cases/migration_spec.rb b/spec/cases/migration_spec.rb index 7777353f..6485ad87 100644 --- a/spec/cases/migration_spec.rb +++ b/spec/cases/migration_spec.rb @@ -7,6 +7,9 @@ self.table_name = 'some' end end + let(:directory) { raise 'NotImplemented' } + let(:migrations_dir) { File.join(FIXTURES_PATH, 'migrations', directory) } + let(:migration_context) { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration, model.connection.internal_metadata) } if ActiveRecord::version >= Gem::Version.new('6.1') connection_config = ActiveRecord::Base.connection_db_config.configuration_hash @@ -14,11 +17,16 @@ connection_config = ActiveRecord::Base.connection_config end + subject do + quietly { migration_context.up } + end + context 'table creation' do context 'plain' do + let(:directory) { 'plain_table_creation' } + it 'creates a table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'plain_table_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -32,9 +40,9 @@ context 'dsl' do context 'empty' do + let(:directory) { 'dsl_table_creation' } it 'creates a table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -45,9 +53,9 @@ end context 'with engine' do + let(:directory) { 'dsl_table_with_engine_creation' } it 'creates a table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_engine_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -61,9 +69,9 @@ context 'types' do context 'decimal' do + let(:directory) { 'dsl_table_with_decimal_creation' } it 'creates a table with valid scale and precision' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_decimal_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -78,9 +86,9 @@ end context 'uuid' do + let(:directory) { 'dsl_table_with_uuid_creation' } it 'creates a table with uuid columns' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_uuid_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -93,9 +101,9 @@ end context 'datetime' do + let(:directory) { 'dsl_table_with_datetime_creation' } it 'creates a table with datetime columns' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_datetime_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -108,9 +116,9 @@ end context 'low_cardinality' do + let(:directory) { 'dsl_table_with_low_cardinality_creation' } it 'creates a table with low cardinality columns' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_low_cardinality_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -125,9 +133,9 @@ end context 'fixed_string' do + let(:directory) { 'dsl_table_with_fixed_string_creation' } it 'creates a table with fixed string columns' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_fixed_string_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -140,9 +148,9 @@ end context 'enum' do + let(:directory) { 'dsl_table_with_enum_creation' } it 'creates a table with enum columns' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_table_with_enum_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -166,11 +174,9 @@ ActiveRecord::Base.establish_connection(connection_config) end + let(:directory) { 'plain_table_creation' } it 'raise error' do - expect { - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'plain_table_creation') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } - }.to raise_error(ActiveRecord::NoDatabaseError) + expect { subject }.to raise_error(ActiveRecord::NoDatabaseError) end end @@ -189,9 +195,9 @@ ActiveRecord::Base.establish_connection(connection_config) end + let(:directory) { 'dsl_create_table_with_distributed' } it 'creates a table with distributed table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_table_with_distributed') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject current_schema = schema(model) current_schema_distributed = schema(model_distributed) @@ -207,14 +213,13 @@ end it 'drops a table with distributed table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_table_with_distributed') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject expect(ActiveRecord::Base.connection.tables).to include('some') expect(ActiveRecord::Base.connection.tables).to include('some_distributed') quietly do - ClickhouseClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).down + migration_context.down end expect(ActiveRecord::Base.connection.tables).not_to include('some') @@ -222,22 +227,24 @@ end end - context 'view' do + context 'creates a view' do + let(:directory) { 'dsl_create_view_with_to_section' } it 'creates a view' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_view_with_to_section') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject expect(ActiveRecord::Base.connection.tables).to include('some_view') end + end + context 'drops a view' do + let(:directory) { 'dsl_create_view_without_to_section' } it 'drops a view' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_view_without_to_section') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject expect(ActiveRecord::Base.connection.tables).to include('some_view') quietly do - ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).down + migration_context.down end expect(ActiveRecord::Base.connection.tables).not_to include('some_view') @@ -265,9 +272,9 @@ ActiveRecord::Base.establish_connection(connection_config) end + let(:directory) { 'dsl_create_table_with_cluster_name_alias' } it 'creates a table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_table_with_cluster_name_alias') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject current_schema = schema(model) @@ -277,13 +284,12 @@ end it 'drops a table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_table_with_cluster_name_alias') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up } + subject expect(ActiveRecord::Base.connection.tables).to include('some') quietly do - ClickhouseClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).down + migration_context.down end expect(ActiveRecord::Base.connection.tables).not_to include('some') @@ -292,13 +298,13 @@ end describe 'drop table' do + let(:directory) { 'dsl_drop_table' } it 'drops table' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_drop_table') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up(1) } + quietly { migration_context.up(1) } expect(ActiveRecord::Base.connection.tables).to include('some') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up(2) } + quietly { migration_context.up(2) } expect(ActiveRecord::Base.connection.tables).not_to include('some') end @@ -318,9 +324,9 @@ end describe 'add column' do + let(:directory) { 'dsl_add_column' } it 'adds a new column' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_add_column') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) @@ -335,9 +341,9 @@ end describe 'drop column' do + let(:directory) { 'dsl_drop_column' } it 'drops column' do - migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_drop_column') - quietly { ClickhouseActiverecord::MigrationContext.new(migrations_dir, model.connection.schema_migration).up } + subject current_schema = schema(model) diff --git a/spec/cases/model_spec.rb b/spec/cases/model_spec.rb index e6e440de..fcf8b323 100644 --- a/spec/cases/model_spec.rb +++ b/spec/cases/model_spec.rb @@ -137,6 +137,7 @@ class Model < ActiveRecord::Base it 'select' do expect(model.count).to eq(2) expect(model.final.count).to eq(1) + expect(model.final.where(date: '2023-07-21').to_sql).to eq('SELECT sample.* FROM sample FINAL WHERE sample.date = \'2023-07-21\'') end end end