Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added dictionary helper #60

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core_extensions/active_record/migration/command_recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ def create_view(*args, &block)
record(:create_view, args, &block)
end

def create_dictionary(*args, &block)
record(:create_dictionary, args, &block)
end

private

def invert_create_view(args)
view_name, options = args
[:drop_table, view_name, options]
end

def invert_create_dictionary(args)
dict_name, options = args
[:drop_dictionary, [dict_name, options]]
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,25 @@ def add_table_options!(create_sql, options)
opts ||= options.options
end

if opts.present?
create_sql << " ENGINE = #{opts}"
else
create_sql << " ENGINE = Log()"
end
return create_sql << " #{opts}" if options.dictionary

create_sql << add_engine!(opts)
create_sql
end

def add_engine!(opts)
return ' ENGINE = Log()' unless opts.present?

match = opts.match(/^Dictionary\((?<database>\w+(?=\.))?(?<table_name>[.\w]+)\)/)

if match
opts[match.begin(:table_name)...match.end(:table_name)] =
"#{current_database}.#{match[:table_name].sub('.', '')}"
end

" ENGINE = #{opts}"
end

def add_as_clause!(create_sql, options)
return unless options.as

Expand Down Expand Up @@ -86,7 +96,7 @@ def add_to_clause!(create_sql, options)
end

def visit_TableDefinition(o)
create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} "
create_sql = +entity_type(o)
create_sql << "IF NOT EXISTS " if o.if_not_exists
create_sql << "#{quote_table_name(o.name)} "
add_to_clause!(create_sql, o) if o.materialized
Expand All @@ -100,6 +110,14 @@ def visit_TableDefinition(o)
create_sql
end

def entity_type(o)
type = 'TABLE'
type = 'VIEW' if o.view
type = 'DICTIONARY' if o.dictionary

"CREATE#{table_modifier_in_create(o)} #{type} "
end

# Returns any SQL string to go between CREATE and TABLE. May be nil.
def table_modifier_in_create(o)
" TEMPORARY" if o.temporary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module ConnectionAdapters
module Clickhouse
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition

attr_reader :view, :materialized, :if_not_exists, :to
attr_reader :view, :materialized, :dictionary, :if_not_exists, :to

def initialize(
conn,
Expand All @@ -17,6 +17,7 @@ def initialize(
comment: nil,
view: false,
materialized: false,
dictionary: false,
to: nil,
**
)
Expand All @@ -33,6 +34,7 @@ def initialize(
@comment = comment
@view = view || materialized
@materialized = materialized
@dictionary = dictionary
@to = to
end

Expand Down
27 changes: 27 additions & 0 deletions lib/active_record/connection_adapters/clickhouse_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,24 @@ def create_database(name)
end
end

def create_dictionary(dict_name, **options, &block)
options.merge!(dictionary: true)
td = create_table_definition(apply_cluster(dict_name), **options)
yield td if block_given?

if options[:force]
drop_dictionary(dict_name, options.merge(if_exists: true))
end

execute schema_creation.accept td

if options[:with_table]
table_name = options.delete(:with_table)
dictionary_options = "Dictionary(#{dict_name})"
create_table(table_name, **options.merge(options: dictionary_options).except(:dictionary), &block)
end
end

def create_view(table_name, **options)
options.merge!(view: true)
options = apply_replica(table_name, options)
Expand Down Expand Up @@ -321,6 +339,15 @@ def drop_table(table_name, options = {}) # :nodoc:
end
end

def drop_dictionary(dict_name, options = {})
do_execute apply_cluster "DROP DICTIONARY#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(dict_name)}"

if options[:with_table]
table_name = options.delete(:with_table)
drop_table(table_name, **options)
end
end

def change_column(table_name, column_name, type, options = {})
result = do_execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}"
raise "Error parse json response: #{result}" if result.presence && !result.is_a?(Hash)
Expand Down
25 changes: 25 additions & 0 deletions spec/cases/migration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,31 @@
expect(ActiveRecord::Base.connection.tables).not_to include('.inner.some_view')
end
end

context 'dictionary' do
it 'creates dictionary with table' do
migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_dictionary_with_table')
quietly { ActiveRecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up }

expect(ActiveRecord::Base.connection.tables).to include('some_dictionary')
expect(ActiveRecord::Base.connection.tables).to include('some_table')
end

it 'drops a dictionary with a table' do
migrations_dir = File.join(FIXTURES_PATH, 'migrations', 'dsl_create_dictionary_with_table')
quietly { ActiveRecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).up }

expect(ActiveRecord::Base.connection.tables).to include('some_dictionary')
expect(ActiveRecord::Base.connection.tables).to include('some_table')

quietly do
ClickhouseActiverecord::MigrationContext.new(migrations_dir, ClickhouseActiverecord::SchemaMigration).down
end

expect(ActiveRecord::Base.connection.tables).not_to include('some_dictionary')
expect(ActiveRecord::Base.connection.tables).not_to include('some_table')
end
end
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateSomeDictionary < ActiveRecord::Migration[5.0]
def change
options = <<~SQL
PRIMARY KEY id
SOURCE(HTTP(url 'http://localhost:3000/file.json' format 'JSONEachRow'))
LAYOUT(FLAT)
LIFETIME(300)
SQL

create_dictionary :some_dictionary, with_table: :some_table, options: options do |t|
t.integer :id, limit: 5, null: false
t.string :name, null: false
t.string :inn, null: false
t.integer :ctn, limit: 5, null: false
end
end
end
13 changes: 10 additions & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ def schema(model)
def clear_db
current_cluster_name = ActiveRecord::Base.connection_db_config.configuration_hash[:cluster_name]
pattern = if current_cluster_name
"DROP TABLE %s ON CLUSTER #{current_cluster_name}"
"DROP %s %s ON CLUSTER #{current_cluster_name}"
else
'DROP TABLE %s'
'DROP %s %s'
end

ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.execute(pattern % table) }
ActiveRecord::Base.connection.tables.each do |table|
%w[TABLE DICTIONARY].each do |type|
ActiveRecord::Base.connection.execute(format(pattern, type, table))
break
rescue ActiveRecord::ActiveRecordError
next
end
end
end

def clear_consts
Expand Down