Skip to content

Commit 2b19fd7

Browse files
ScotterCclaude
andauthored
Add JSON column type support (#209)
* Add JSON column type support Adds support for ClickHouse JSON column type to fix schema dump failures. - Add json: { name: 'JSON' } to NATIVE_DATABASE_TYPES constant - Register JSON type mapping using ActiveRecord::Type::Json - Add comprehensive test coverage for JSON column operations - Include tests for column recognition, CRUD operations, and migrations - Handle ClickHouse JSON type limitations (non-nullable, numbers as strings) Resolves "Unknown type 'JSON' for column" errors in Rails schema dumps. Requires allow_experimental_json_type = 1 setting in ClickHouse 21.1+. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix JSON migration test for ClickHouse 25.10 compatibility Use MergeTree engine for JSON migration test instead of relying on the default Log engine. The Log engine doesn't support JSON columns with dynamic subcolumns in ClickHouse, which causes test failures in version 25.10. This change ensures compatibility with both ClickHouse 24.9 and 25.10. All 98 tests now pass on both versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3301f6b commit 2b19fd7

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

lib/active_record/connection_adapters/clickhouse_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ class ClickhouseAdapter < AbstractAdapter
113113
uint64: { name: 'UInt64' },
114114
# uint128: { name: 'UInt128' }, not yet implemented in clickhouse
115115
uint256: { name: 'UInt256' },
116+
117+
json: { name: 'JSON' },
116118
}.freeze
117119

118120
include Clickhouse::SchemaStatements
@@ -246,6 +248,8 @@ def initialize_type_map(m) # :nodoc:
246248
m.register_type(%r(Map)) do |sql_type|
247249
Clickhouse::OID::Map.new(sql_type)
248250
end
251+
252+
m.register_type %r(JSON)i, ActiveRecord::Type::Json.new
249253
end
250254
end
251255

spec/single/model_spec.rb

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,4 +631,155 @@ class ModelWithoutPrimaryKey < ActiveRecord::Base
631631
end
632632
end
633633
end
634+
635+
context 'json' do
636+
let!(:json_model) do
637+
Class.new(ActiveRecord::Base) do
638+
self.table_name = 'json_test_table'
639+
end
640+
end
641+
642+
before do
643+
# Create table with JSON column
644+
json_model.connection.execute('DROP TABLE IF EXISTS json_test_table')
645+
json_model.connection.execute(<<~SQL, nil, settings: { allow_experimental_json_type: 1 })
646+
CREATE TABLE json_test_table (
647+
id UInt64,
648+
properties JSON,
649+
metadata JSON
650+
) ENGINE = MergeTree ORDER BY id
651+
SQL
652+
end
653+
654+
after do
655+
json_model.connection.execute('DROP TABLE IF EXISTS json_test_table')
656+
end
657+
658+
describe 'JSON column type recognition' do
659+
it 'recognizes JSON columns with correct type' do
660+
columns = json_model.columns_hash
661+
expect(columns['properties'].type).to eq(:json)
662+
expect(columns['properties'].sql_type).to eq('JSON')
663+
expect(columns['metadata'].type).to eq(:json)
664+
end
665+
666+
it 'validates JSON type in connection' do
667+
connection = json_model.connection
668+
expect(connection.valid_type?(:json)).to be_truthy
669+
expect(connection.native_database_types[:json]).to eq({ name: 'JSON' })
670+
end
671+
end
672+
673+
describe 'JSON data operations' do
674+
it 'creates record with JSON data' do
675+
test_data = { 'key' => 'value', 'nested' => { 'count' => '42' } }
676+
metadata = { 'version' => '1.0', 'tags' => ['test', 'json'] }
677+
678+
expect {
679+
json_model.create!(
680+
id: 1,
681+
properties: test_data,
682+
metadata: metadata
683+
)
684+
}.to change { json_model.count }.by(1)
685+
686+
record = json_model.first
687+
expect(record.properties).to eq(test_data)
688+
expect(record.metadata).to eq(metadata)
689+
end
690+
691+
it 'handles empty JSON values' do
692+
expect {
693+
json_model.create!(
694+
id: 2,
695+
properties: {},
696+
metadata: { 'status' => 'empty' }
697+
)
698+
}.to change { json_model.count }.by(1)
699+
700+
record = json_model.first
701+
expect(record.properties).to eq({})
702+
expect(record.metadata).to eq({ 'status' => 'empty' })
703+
end
704+
705+
it 'handles complex JSON structures' do
706+
# Note: In ClickHouse JSON type, numbers are stored as strings
707+
complex_json = {
708+
'user' => {
709+
'name' => 'John Doe',
710+
'preferences' => {
711+
'theme' => 'dark',
712+
'notifications' => true,
713+
'languages' => ['en', 'es']
714+
}
715+
},
716+
'timestamps' => {
717+
'created_at' => '2023-01-01T00:00:00Z',
718+
'updated_at' => '2023-12-31T23:59:59Z'
719+
},
720+
'metrics' => ['1', '2', '3', '4', '5'], # Numbers become strings in ClickHouse JSON
721+
'active' => true,
722+
'score' => 0.955e2 # ClickHouse JSON representation
723+
}
724+
725+
json_model.create!(
726+
id: 3,
727+
properties: complex_json,
728+
metadata: { 'type' => 'complex' }
729+
)
730+
731+
record = json_model.first
732+
expect(record.properties['user']['name']).to eq('John Doe')
733+
expect(record.properties['metrics']).to eq(['1', '2', '3', '4', '5'])
734+
expect(record.properties['active']).to be_truthy
735+
expect(record.properties['score']).to be_a(Numeric)
736+
end
737+
738+
it 'works with insert_all' do
739+
records = [
740+
{ id: 4, properties: { 'batch' => '1' }, metadata: { 'source' => 'batch' } },
741+
{ id: 5, properties: { 'batch' => '2' }, metadata: { 'source' => 'batch' } }
742+
]
743+
744+
expect {
745+
json_model.insert_all(records)
746+
}.to change { json_model.count }.by(2)
747+
748+
first_record = json_model.find_by(id: 4)
749+
second_record = json_model.find_by(id: 5)
750+
751+
expect(first_record.properties).to eq({ 'batch' => '1' })
752+
expect(second_record.properties).to eq({ 'batch' => '2' })
753+
expect(first_record.metadata).to eq({ 'source' => 'batch' })
754+
end
755+
end
756+
757+
describe 'migration and schema dumping' do
758+
it 'allows creating tables with JSON columns via migration' do
759+
# Create a temporary migration-style table
760+
json_model.connection.execute('DROP TABLE IF EXISTS migration_json_test')
761+
762+
expect {
763+
json_model.connection.create_table :migration_json_test, id: false,
764+
options: 'MergeTree ORDER BY id',
765+
request_settings: { allow_experimental_json_type: 1 } do |t|
766+
t.column :id, :integer, null: false
767+
t.json :config, null: false
768+
t.json :optional_data, null: false # JSON columns cannot be nullable in ClickHouse
769+
end
770+
}.not_to raise_error
771+
772+
# Verify the table was created with correct column types
773+
columns = json_model.connection.columns('migration_json_test')
774+
config_column = columns.find { |c| c.name == 'config' }
775+
optional_column = columns.find { |c| c.name == 'optional_data' }
776+
777+
expect(config_column.type).to eq(:json)
778+
expect(config_column.sql_type).to eq('JSON')
779+
expect(optional_column.type).to eq(:json)
780+
781+
json_model.connection.execute('DROP TABLE IF EXISTS migration_json_test')
782+
end
783+
end
784+
end
634785
end

0 commit comments

Comments
 (0)