Skip to content

Commit

Permalink
database create / drop, migration support with rake tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
PNixx committed Feb 26, 2019
1 parent bce303d commit 7a72b94
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 48 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Clickhouse::Activerecord

A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.0.
A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.

## Installation

Expand Down Expand Up @@ -59,10 +59,30 @@ development:
password: password
```
Schema dump:
### Rake tasks
Create / drop / purge / reset database:
$ rake clickhouse:create
$ rake clickhouse:drop
$ rake clickhouse:purge
$ rake clickhouse:reset
Migration:
$ rails g clickhouse_migration MIGRATION_NAME COLUMNS
$ rake clickhouse:migrate
Rollback migration not supported!
Schema dump to `db/clickhouse_schema.rb` file:

$ rake clickhouse:schema:dump

Schema load from `db/clickhouse_schema.rb` file:

$ rake clickhouse:schema:load

We use schema for emulate development or tests environment on PostgreSQL adapter.

### Insert and select data
Expand All @@ -81,8 +101,6 @@ ActionView.maximum(:date)
#=> 'Wed, 29 Nov 2017'
```

NOTE: Creating tables in developing.

## Donations

Donations to this project are going directly to [PNixx](https://github.com/PNixx), the original author of this project:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def visit_AddColumnDefinition(o)
end

def add_column_options!(sql, options)
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
if options[:null] || options[:null].nil?
sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def process_response(res)
end

def log_with_debug(sql, name = nil)
return yield unless self.pool.spec.config[:debug]
return yield unless @debug
log(sql, "#{name} (system)") { yield }
end

Expand All @@ -127,7 +127,10 @@ def create_table_definition(*args)
def new_column_from_field(table_name, field)
sql_type = field[1]
type_metadata = fetch_type_metadata(sql_type)
ClickhouseColumn.new(field[0], field[3].present? ? field[3] : nil, type_metadata, field[1].include?('Nullable'), table_name, nil)
default = field[3]
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
ClickhouseColumn.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), table_name, default_function)
end

protected
Expand All @@ -142,6 +145,35 @@ def table_structure(table_name)
"Could not find table '#{table_name}'"
end
alias column_definitions table_structure

private

# Extracts the value from a PostgreSQL column default definition.
def extract_value_from_default(default)
case default
# Quoted types
when /\Anow\(\)\z/m
nil
# Boolean types
when "true".freeze, "false".freeze
default
# Object identifier types
when /\A-?\d+\z/
$1
else
# Anything else is blank, some user type, or some function
# and we can't know the value of that, so return nil.
nil
end
end

def extract_default_function(default_value, default) # :nodoc:
default if has_default_function?(default_value, default)
end

def has_default_function?(default_value, default) # :nodoc:
!default_value && (%r{\w+\(.*\)} === default)
end
end
end
end
Expand Down
64 changes: 25 additions & 39 deletions lib/active_record/connection_adapters/clickhouse_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def clickhouse_connection(config)
raise ArgumentError, 'No database specified. Missing argument: database.'
end

ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact)
ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact, config[:debug])
end
end
end
Expand All @@ -45,43 +45,6 @@ def is_view=(value)
module ConnectionAdapters
class ClickhouseColumn < Column

private

# Extracts the value from a PostgreSQL column default definition.
def extract_value_from_default(default)
case default
# Quoted types
when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
# The default 'now'::date is CURRENT_DATE
if $1 == "now".freeze && $2 == "date".freeze
nil
else
$1.gsub("''".freeze, "'".freeze)
end
# Boolean types
when "true".freeze, "false".freeze
default
# Numeric types
when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
$1
# Object identifier types
when /\A-?\d+\z/
$1
else
# Anything else is blank, some user type, or some function
# and we can't know the value of that, so return nil.
nil
end
end

def extract_default_function(default_value, default) # :nodoc:
default if has_default_function?(default_value, default)
end

def has_default_function?(default_value, default) # :nodoc:
!default_value && (%r{\w+\(.*\)} === default)
end

end

class ClickhouseAdapter < AbstractAdapter
Expand All @@ -101,10 +64,11 @@ class ClickhouseAdapter < AbstractAdapter
include Clickhouse::SchemaStatements

# Initializes and connects a Clickhouse adapter.
def initialize(connection, logger, connection_parameters, config)
def initialize(connection, logger, connection_parameters, config, debug = false)
super(connection, logger)
@connection_parameters = connection_parameters
@config = config
@debug = debug

@prepared_statements = false

Expand Down Expand Up @@ -169,6 +133,28 @@ def create_schema_dumper(options) # :nodoc:
ClickhouseActiverecord::SchemaDumper.create(self, options)
end

# Create a new ClickHouse database.
def create_database(name)
sql = "CREATE DATABASE #{quote_table_name(name)}"
log_with_debug(sql, adapter_name) do
res = @connection.post("/?#{@config.except(:database).to_param}", "CREATE DATABASE #{quote_table_name(name)}")
process_response(res)
end
end

# Drops a ClickHouse database.
def drop_database(name) #:nodoc:
sql = "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
log_with_debug(sql, adapter_name) do
res = @connection.post("/?#{@config.except(:database).to_param}", sql)
process_response(res)
end
end

def drop_table(table_name, options = {}) # :nodoc:
do_execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end

protected

def last_inserted_id(result)
Expand Down
2 changes: 2 additions & 0 deletions lib/clickhouse-activerecord.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
if defined?(Rails::Railtie)
require 'clickhouse-activerecord/railtie'
require 'clickhouse-activerecord/schema_dumper'
require 'clickhouse-activerecord/tasks'
ActiveRecord::Tasks::DatabaseTasks.register_task(/clickhouse/, "ClickhouseActiverecord::Tasks")
end

module ClickhouseActiverecord
Expand Down
65 changes: 65 additions & 0 deletions lib/clickhouse-activerecord/tasks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module ClickhouseActiverecord
class Tasks

delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base

def initialize(configuration)
@configuration = configuration
end

def create
establish_master_connection
connection.create_database @configuration["database"]
rescue ActiveRecord::StatementInvalid => e
if e.cause.to_s.include?('already exists')
raise ActiveRecord::Tasks::DatabaseAlreadyExists
else
raise
end
end

def drop
establish_master_connection
connection.drop_database @configuration["database"]
end

def purge
clear_active_connections!
drop
create
end

def migrate
check_target_version

verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
scope = ENV["SCOPE"]
verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, verbose
binding.pry
connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
ActiveRecord::Base.clear_cache!
ensure
ActiveRecord::Migration.verbose = verbose_was
end

private

def establish_master_connection
establish_connection @configuration
end

def check_target_version
if target_version && !(ActiveRecord::Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
end
end

def target_version
ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
end
end
end
2 changes: 1 addition & 1 deletion lib/clickhouse-activerecord/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ClickhouseActiverecord
VERSION = '0.3.0'
VERSION = '0.3.1'
end
11 changes: 11 additions & 0 deletions lib/generators/clickhouse_migration_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'rails/generators/active_record/migration/migration_generator'

class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates")

def create_migration_file
set_local_assigns!
validate_file_name!
migration_template @migration_template, "db/migrate_clickhouse/#{file_name}.rb"
end
end
34 changes: 32 additions & 2 deletions lib/tasks/clickhouse.rake
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

namespace :clickhouse do

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")
end

namespace :schema do

# todo not testing
desc 'Load database schema'
task load: :environment do
ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
task load: :load_config do
load("#{Rails.root}/db/clickhouse_schema.rb")
end

Expand All @@ -22,4 +27,29 @@ namespace :clickhouse do

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"])
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"])
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"])
end

# desc 'Resets your database using your migrations for the current environment'
task reset: :load_config do
Rake::Task['clickhouse:purge'].execute
Rake::Task['clickhouse:migrate'].execute
end

desc 'Migrate the clickhouse database'
task migrate: :load_config do
Rake::Task['db:migrate'].execute
end
end

0 comments on commit 7a72b94

Please sign in to comment.