diff --git a/lib/active_record/connection_adapters/clickhouse/schema_creation.rb b/lib/active_record/connection_adapters/clickhouse/schema_creation.rb index 4af0d4e..b5f7b9a 100644 --- a/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +++ b/lib/active_record/connection_adapters/clickhouse/schema_creation.rb @@ -82,6 +82,19 @@ def assign_database_to_subquery!(subquery) "#{current_database}.#{match[:table_name].sub('.', '')}" end + def add_materialized_to_clause!(create_sql, options) + if !options.to + create_sql << " ENGINE = Memory()" + else + target_table = options.to.split('.').last + table_structure = @conn.execute("DESCRIBE TABLE #{target_table}")['data'] + column_definitions = table_structure.map do |field| + "`#{field[0]}` #{field[1]}" + end + create_sql << "TO #{options.to} (#{column_definitions.join(', ')}) " + end + end + def add_to_clause!(create_sql, options) # If you do not specify a database explicitly, ClickHouse will use the "default" database. return unless options.to @@ -97,23 +110,34 @@ def visit_TableDefinition(o) create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} " create_sql << "IF NOT EXISTS " if o.if_not_exists create_sql << "#{quote_table_name(o.name)} " - add_as_clause!(create_sql, o) if o.as && !o.view - add_to_clause!(create_sql, o) if o.materialized - statements = o.columns.map { |c| accept c } - statements << accept(o.primary_keys) if o.primary_keys + # Add column definitions for regular tables only + if !o.view && o.columns.present? + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys - if supports_indexes_in_create? - indexes = o.indexes.map do |expression, options| - accept(@conn.add_index_options(o.name, expression, **options)) + if supports_indexes_in_create? + indexes = o.indexes.map do |expression, options| + accept(@conn.add_index_options(o.name, expression, **options)) + end + statements.concat(indexes) end - statements.concat(indexes) + + create_sql << "(#{statements.join(', ')})" end - create_sql << "(#{statements.join(', ')})" if statements.present? - # Attach options for only table or materialized view without TO section - add_table_options!(create_sql, o) if !o.view || o.view && o.materialized && !o.to - add_as_clause!(create_sql, o) if o.as && o.view + # Add TO clause for materialized views before AS clause + add_materialized_to_clause!(create_sql, o) if o.materialized && o.view + + # Add AS clause for all views + add_as_clause!(create_sql, o) if o.as + + # Add TO clause for regular views (non-materialized) after AS clause + add_to_clause!(create_sql, o) if o.to && !o.materialized + + # Add table options for regular tables + add_table_options!(create_sql, o) if !o.view + create_sql end diff --git a/spec/single/materialized_view_spec.rb b/spec/single/materialized_view_spec.rb new file mode 100644 index 0000000..760d72f --- /dev/null +++ b/spec/single/materialized_view_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +RSpec.describe 'Materialized Views' do + before do + ActiveRecord::Schema.define do + create_table "events", id: false, options: "Log", force: :cascade do |t| + t.integer "quantity", default: -> { "CAST(1, 'Int8')" }, null: false + t.string "name", null: false + t.date "created_at", null: false + end + end + end + + after do + ActiveRecord::Schema.define do + drop_table :events if table_exists?(:events) + drop_table :aggregated_events_mv if table_exists?(:aggregated_events_mv) + drop_table :aggregated_events if table_exists?(:aggregated_events) + end + end + + it 'creates a materialized view with TO clause and column definitions' do + database = ActiveRecord::Base.connection_db_config.database + + ActiveRecord::Schema.define do + create_table "aggregated_events", id: false, options: "SummingMergeTree ORDER BY (name, date) SETTINGS index_granularity = 8192", force: :cascade do |t| + t.string "name", null: false + t.date "date", null: false + t.integer "total_quantity", limit: 8, null: false + t.integer "event_count", limit: 8, null: false + end + + create_table "aggregated_events_mv", view: true, materialized: true, to: "#{database}.aggregated_events", id: false, as: "SELECT name, created_at AS date, sum(quantity) AS total_quantity, count() AS event_count FROM #{database}.events GROUP BY name, created_at", force: :cascade do |t| + end + end + + # Verify the view was created correctly + result = ActiveRecord::Base.connection.do_system_execute( + "SHOW CREATE TABLE #{database}.aggregated_events_mv" + )['data'].first.first + + expect(result.squish).to eq('CREATE MATERIALIZED VIEW default.aggregated_events_mv TO default.aggregated_events ( `name` String, `date` Date, `total_quantity` UInt64, `event_count` UInt64 ) AS SELECT name, created_at AS date, sum(quantity) AS total_quantity, count() AS event_count FROM default.events GROUP BY name, created_at') + end +end