Skip to content

Commit fb548b8

Browse files
committed
support cse in the with clause
1 parent c4a41d1 commit fb548b8

File tree

7 files changed

+97
-11
lines changed

7 files changed

+97
-11
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
### Version 1.6.0 (Jan 19, 2026)
2+
3+
* Support CSE (Common Scalar Expressions) in the `WITH` clause
4+
* Fix regex to match FROM keyword, not column names containing 'from' #220
5+
* Add JSON column type support #209
6+
* Support execute_batch #216
7+
* Add disconnect method to adapter #186
8+
* Add check_current_protected_environment! to Tasks #222
9+
* Do not truncate engines that cannot be truncated #226
10+
111
### Version 1.5.1 (Nov 6, 2025)
212

313
* Fix rake tasks

lib/arel/visitors/clickhouse.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,33 @@ def aggregate(name, o, collector)
1818
end
1919
end
2020

21+
# https://clickhouse.com/docs/sql-reference/statements/select/with
22+
def visit_Arel_Nodes_Cte(o, collector)
23+
is_cse = o.relation.is_a?(Symbol)
24+
25+
if is_cse && o.name.is_a?(String)
26+
collector << quote(o.name)
27+
elsif is_cse && o.name.is_a?(ActiveRecord::Relation)
28+
visit o.name.arel, collector
29+
else
30+
collector << quote_table_name(o.name)
31+
end
32+
collector << " AS "
33+
34+
case o.materialized
35+
when true
36+
collector << "MATERIALIZED "
37+
when false
38+
collector << "NOT MATERIALIZED "
39+
end
40+
41+
if is_cse
42+
collector << o.relation.to_s
43+
else
44+
visit o.relation, collector
45+
end
46+
end
47+
2148
# https://clickhouse.com/docs/en/sql-reference/statements/delete
2249
# DELETE and UPDATE in ClickHouse working only without table name
2350
def visit_Arel_Attributes_Attribute(o, collector)

lib/clickhouse-activerecord.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
require 'core_extensions/active_record/internal_metadata'
66
require 'core_extensions/active_record/relation'
7+
require 'core_extensions/active_record/relation/query_methods'
78
require 'core_extensions/active_record/schema_migration'
89
require 'core_extensions/active_record/migration/command_recorder'
910
require 'core_extensions/arel/nodes/select_core'
@@ -24,6 +25,7 @@ def self.load
2425
ActiveRecord::InternalMetadata.prepend(CoreExtensions::ActiveRecord::InternalMetadata)
2526
ActiveRecord::Migration::CommandRecorder.include(CoreExtensions::ActiveRecord::Migration::CommandRecorder)
2627
ActiveRecord::Relation.prepend(CoreExtensions::ActiveRecord::Relation)
28+
ActiveRecord::QueryMethods.prepend(CoreExtensions::ActiveRecord::QueryMethods)
2729
ActiveRecord::SchemaMigration.prepend(CoreExtensions::ActiveRecord::SchemaMigration)
2830

2931
Arel::Nodes::SelectCore.prepend(CoreExtensions::Arel::Nodes::SelectCore)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module ClickhouseActiverecord
2-
VERSION = '1.5.1'
2+
VERSION = '1.6.0'
33
end

lib/core_extensions/active_record/relation.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,6 @@ def self.prepended(base)
66
base::VALID_UNSCOPING_VALUES << :final << :settings
77
end
88

9-
def reverse_order!
10-
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
11-
12-
orders = order_values.uniq.reject(&:blank?)
13-
return super unless orders.empty? && !primary_key
14-
15-
self.order_values = (column_names & %w[date created_at]).map { |c| arel_table[c].desc }
16-
self
17-
end
18-
199
# 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.
2010
# For example:
2111
#
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module CoreExtensions
2+
module ActiveRecord
3+
module QueryMethods
4+
5+
def reverse_order!
6+
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
7+
8+
orders = order_values.uniq.reject(&:blank?)
9+
return super unless orders.empty? && !primary_key
10+
11+
self.order_values = (column_names & %w[date created_at]).map { |c| arel_table[c].desc }
12+
self
13+
end
14+
15+
def build_with_expression_from_value(value, nested = false)
16+
case value
17+
when Symbol
18+
value
19+
else
20+
super
21+
end
22+
end
23+
24+
end
25+
end
26+
end

spec/single/model_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,37 @@ class ModelPk < ActiveRecord::Base
467467
expect(sql).to eq('SELECT sample.* FROM sample GROUP BY GROUPING SETS ( ( foo, bar ), ( sample.baz ) )')
468468
end
469469
end
470+
471+
describe '#cte & cse:' do
472+
it 'cte string' do
473+
sql = Model.with('t' => ModelJoin.where(event_name: 'test')).where(event_name: Model.from('f').select('event_name')).to_sql
474+
expect(sql).to eq('WITH t AS (SELECT joins.* FROM joins WHERE joins.event_name = \'test\') SELECT sample.* FROM sample WHERE sample.event_name IN (SELECT event_name FROM f)')
475+
end
476+
477+
it 'cte symbol' do
478+
sql = Model.with(t: ModelJoin.where(event_name: 'test')).where(event_name: Model.from('f').select('event_name')).to_sql
479+
expect(sql).to eq('WITH t AS (SELECT joins.* FROM joins WHERE joins.event_name = \'test\') SELECT sample.* FROM sample WHERE sample.event_name IN (SELECT event_name FROM f)')
480+
end
481+
482+
it 'cse string variable' do
483+
sql = Model.with('2026-01-01 15:23:00' => :t).where(Arel.sql('date = toDate(t)')).to_sql
484+
expect(sql).to eq('WITH \'2026-01-01 15:23:00\' AS t SELECT sample.* FROM sample WHERE (date = toDate(t))')
485+
end
486+
487+
it 'cse symbol function' do
488+
sql = Model.with('(id, extension) -> concat(lower(id), extension)': :t).where(Arel.sql('date = toDate(t)')).to_sql
489+
expect(sql).to eq('WITH (id, extension) -> concat(lower(id), extension) AS t SELECT sample.* FROM sample WHERE (date = toDate(t))')
490+
end
491+
492+
it 'cse query relation' do
493+
sql = Model.with(ModelJoin.select(Arel.sql('min(date)')) => :min_date).where(Arel.sql('date = min_date')).to_sql
494+
expect(sql).to eq('WITH (SELECT min(date) FROM joins) AS min_date SELECT sample.* FROM sample WHERE (date = min_date)')
495+
end
496+
497+
it 'cse error' do
498+
expect { Model.with('2026-01-01 15:23:00' => 't').where(Arel.sql('date = toDate(t)')).to_sql }.to raise_error(ArgumentError)
499+
end
500+
end
470501
end
471502

472503
context 'sample with id column' do

0 commit comments

Comments
 (0)