diff options
Diffstat (limited to 'spec/lib/click_house')
-rw-r--r-- | spec/lib/click_house/bind_index_manager_spec.rb | 33 | ||||
-rw-r--r-- | spec/lib/click_house/query_builder_spec.rb | 334 | ||||
-rw-r--r-- | spec/lib/click_house/redactor_spec.rb | 166 |
3 files changed, 533 insertions, 0 deletions
diff --git a/spec/lib/click_house/bind_index_manager_spec.rb b/spec/lib/click_house/bind_index_manager_spec.rb new file mode 100644 index 00000000000..1c659017c63 --- /dev/null +++ b/spec/lib/click_house/bind_index_manager_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ClickHouse::BindIndexManager, feature_category: :database do + describe '#next_bind_str' do + context 'when initialized without a start index' do + let(:bind_manager) { described_class.new } + + it 'starts from index 1 by default' do + expect(bind_manager.next_bind_str).to eq('$1') + end + + it 'increments the bind string on subsequent calls' do + bind_manager.next_bind_str + expect(bind_manager.next_bind_str).to eq('$2') + end + end + + context 'when initialized with a start index' do + let(:bind_manager) { described_class.new(2) } + + it 'starts from the given index' do + expect(bind_manager.next_bind_str).to eq('$2') + end + + it 'increments the bind string on subsequent calls' do + bind_manager.next_bind_str + expect(bind_manager.next_bind_str).to eq('$3') + end + end + end +end diff --git a/spec/lib/click_house/query_builder_spec.rb b/spec/lib/click_house/query_builder_spec.rb new file mode 100644 index 00000000000..9e3f1118eeb --- /dev/null +++ b/spec/lib/click_house/query_builder_spec.rb @@ -0,0 +1,334 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ClickHouse::QueryBuilder, feature_category: :database do + let(:table_name) { :test_table } + let(:builder) { described_class.new(table_name) } + + shared_examples "generates correct sql on multiple calls to `to_sql`" do |method_name, argument1, argument2| + it 'returns the same SQL when called multiple times on the same builder' do + query_builder = builder.public_send(method_name, argument1) + first_sql = query_builder.to_sql + second_sql = query_builder.to_sql + + expect(first_sql).to eq(second_sql) + end + + it 'returns different SQL when called multiple times on different builders' do + query_builder = builder.public_send(method_name, argument1) + query_builder_2 = query_builder.public_send(method_name, argument2) + + first_sql = query_builder.to_sql + second_sql = query_builder_2.to_sql + + expect(first_sql).not_to eq(second_sql) + end + end + + describe "#initialize" do + it 'initializes with correct table' do + expect(builder.table.name).to eq(table_name.to_s) + end + end + + describe '#where' do + context 'with simple conditions' do + it 'builds correct where query' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" = 'value1' + AND "test_table"."column2" = 'value2' + SQL + + sql = builder.where(column1: 'value1', column2: 'value2').to_sql + + expect(sql).to eq(expected_sql) + end + end + + context 'with array conditions' do + it 'builds correct where query' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" IN (1, 2, 3) + SQL + + sql = builder.where(column1: [1, 2, 3]).to_sql + + expect(sql).to eq(expected_sql) + end + end + + it_behaves_like "generates correct sql on multiple calls to `to_sql`", :where, { column1: 'value1' }, + { column2: 'value2' } + + context 'with supported arel nodes' do + it 'builds a query using the In node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" IN ('value1', 'value2') + SQL + + sql = builder.where(builder.table[:column1].in(%w[value1 value2])).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds a query using the Equality node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" = 'value1' + SQL + + sql = builder.where(builder.table[:column1].eq('value1')).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds a query using the LessThan node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" < 5 + SQL + + sql = builder.where(builder.table[:column1].lt(5)).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds a query using the LessThanOrEqual node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" <= 5 + SQL + + sql = builder.where(builder.table[:column1].lteq(5)).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds a query using the GreaterThan node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" > 5 + SQL + + sql = builder.where(builder.table[:column1].gt(5)).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds a query using the GreaterThanOrEqual node' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" >= 5 + SQL + + sql = builder.where(builder.table[:column1].gteq(5)).to_sql + + expect(sql).to eq(expected_sql) + end + end + + context 'with unsupported arel nodes' do + it 'raises an error for the unsupported node' do + expect do + builder.where(builder.table[:column1].not_eq('value1')).to_sql + end.to raise_error(ArgumentError, /Unsupported Arel node type for QueryBuilder:/) + end + end + end + + describe '#select' do + it 'builds correct select query with single field' do + expected_sql = <<~SQL.chomp + SELECT "test_table"."column1" FROM "test_table" + SQL + + sql = builder.select(:column1).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds correct select query with multiple fields' do + expected_sql = <<~SQL.chomp + SELECT "test_table"."column1", "test_table"."column2" FROM "test_table" + SQL + + sql = builder.select(:column1, :column2).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'adds new fields on multiple calls without duplicating' do + expected_sql = <<~SQL.chomp + SELECT "test_table"."column1", "test_table"."column2" FROM "test_table" + SQL + + sql = builder.select(:column1).select(:column2).select(:column1).to_sql + + expect(sql).to eq(expected_sql) + end + + it_behaves_like "generates correct sql on multiple calls to `to_sql`", :select, :column1, :column2 + end + + describe '#order' do + it 'builds correct order query with direction :desc' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + ORDER BY "test_table"."column1" DESC + SQL + + sql = builder.order(:column1, :desc).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'builds correct order query with default direction asc' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + ORDER BY "test_table"."column1" ASC + SQL + + sql = builder.order(:column1).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'appends orderings on multiple calls' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + ORDER BY "test_table"."column1" DESC, + "test_table"."column2" ASC + SQL + + sql = builder.order(:column1, :desc).order(:column2, :asc).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'appends orderings for the same column when ordered multiple times' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + ORDER BY "test_table"."column1" DESC, + "test_table"."column1" ASC + SQL + + sql = builder.order(:column1, :desc).order(:column1, :asc).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'raises error for invalid direction' do + expect do + builder.order(:column1, :invalid) + end.to raise_error(ArgumentError, "Invalid order direction 'invalid'. Must be :asc or :desc") + end + + it_behaves_like "generates correct sql on multiple calls to `to_sql`", :order, :column1, :column2 + end + + describe '#limit' do + it 'builds correct limit query' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + LIMIT 10 + SQL + + sql = builder.limit(10).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'overrides previous limit value when called multiple times' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + LIMIT 20 + SQL + + sql = builder.limit(10).limit(20).to_sql + + expect(sql).to eq(expected_sql) + end + end + + describe '#offset' do + it 'builds correct offset query' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + OFFSET 5 + SQL + + sql = builder.offset(5).to_sql + + expect(sql).to eq(expected_sql) + end + + it 'overrides previous offset value when called multiple times' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + OFFSET 10 + SQL + + sql = builder.offset(5).offset(10).to_sql + + expect(sql).to eq(expected_sql) + end + end + + describe '#to_sql' do + it 'delegates to the Arel::SelectManager' do + expect(builder.send(:manager)).to receive(:to_sql) + + builder.to_sql + end + end + + describe '#to_redacted_sql' do + it 'calls ::ClickHouse::Redactor correctly' do + expect(::ClickHouse::Redactor).to receive(:redact).with(builder) + + builder.to_redacted_sql + end + end + + describe '#apply_conditions!' do + it 'applies conditions to the manager' do + manager = builder.send(:manager) + condition = Arel::Nodes::Equality.new(builder.table[:column1], 'value1') + builder.conditions << condition + + expect(manager).to receive(:where).with(condition) + + builder.send(:apply_conditions!) + end + end + + describe 'method chaining', :freeze_time do + it 'builds correct SQL query when methods are chained' do + expected_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT "test_table"."column1", "test_table"."column2" + FROM "test_table" + WHERE "test_table"."column1" = 'value1' + AND "test_table"."column2" = 'value2' + AND "test_table"."created_at" <= '#{Date.today}' + ORDER BY "test_table"."column1" DESC + LIMIT 10 + OFFSET 5 + SQL + + sql = builder + .select(:column1, :column2) + .where(column1: 'value1', column2: 'value2') + .where(builder.table[:created_at].lteq(Date.today)) + .order(:column1, 'desc') + .limit(10) + .offset(5) + .to_sql + + expect(sql).to eq(expected_sql) + end + end +end diff --git a/spec/lib/click_house/redactor_spec.rb b/spec/lib/click_house/redactor_spec.rb new file mode 100644 index 00000000000..d8354b6cbb9 --- /dev/null +++ b/spec/lib/click_house/redactor_spec.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ClickHouse::Redactor, feature_category: :database do + let(:builder) { ClickHouse::QueryBuilder.new(:test_table) } + + describe '.redact' do + context 'when given simple conditions' do + let(:new_builder) { builder.where(column1: 'value1', column2: 'value2') } + let(:redacted_query) { described_class.redact(new_builder) } + + it 'redacts equality conditions correctly' do + expected_redacted_sql = <<~SQL.chomp.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" = $1 + AND "test_table"."column2" = $2 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + end + + context 'when given IN conditions' do + let(:new_builder) { builder.where(column1: %w[value1 value2 value3]) } + let(:redacted_query) { described_class.redact(new_builder) } + + it 'redacts IN conditions correctly' do + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" IN ($1, $2, $3) + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + end + + context 'with supported arel nodes' do + it 'redacts a query using the In node' do + new_builder = builder.where(builder.table[:column1].in(%w[value1 value2])) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" IN ($1, $2) + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + it 'redacts a query using the Equality node' do + new_builder = builder.where(builder.table[:column1].eq('value1')) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" = $1 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + it 'redacts a query using the LessThan node' do + new_builder = builder.where(builder.table[:column1].lt(5)) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" < $1 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + it 'redacts a query using the LessThanOrEqual node' do + new_builder = builder.where(builder.table[:column1].lteq(5)) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" <= $1 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + it 'redacts a query using the GreaterThan node' do + new_builder = builder.where(builder.table[:column1].gt(5)) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" > $1 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + it 'redacts a query using the GreaterThanOrEqual node' do + new_builder = builder.where(builder.table[:column1].gteq(5)) + redacted_query = described_class.redact(new_builder) + + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" >= $1 + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + end + + context 'with unsupported arel nodes' do + let(:unsupported_node) { Arel::Nodes::NotEqual.new(Arel::Table.new(:test_table)[:column1], 'value1') } + let(:manager) do + instance_double( + 'Arel::SelectManager', + constraints: [], + where: true, + to_sql: "SELECT * FROM \"test_table\"" + ) + end + + let(:mocked_builder) do + instance_double( + 'ClickHouse::QueryBuilder', + conditions: [unsupported_node], + manager: manager + ) + end + + it 'raises an error for the unsupported node' do + expect do + described_class.redact(mocked_builder) + end.to raise_error(ArgumentError, /Unsupported Arel node type for Redactor:/) + end + end + + context 'when method chaining is used' do + let(:new_builder) do + builder.where(column1: 'value1').where(column2: 'value2').where(builder.table[:column3].gteq(5)) + end + + let(:redacted_query) { described_class.redact(new_builder) } + + it 'redacts chained conditions correctly' do + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" + WHERE "test_table"."column1" = $1 + AND "test_table"."column2" = $2 + AND "test_table"."column3" >= $3 + SQL + expect(redacted_query).to eq(expected_redacted_sql) + end + end + + context 'when calling .redact multiple times' do + let(:new_builder) { builder.where(column1: 'value1', column2: 'value2') } + let(:first_redacted_query) { described_class.redact(new_builder) } + let(:second_redacted_query) { described_class.redact(new_builder) } + + it 'produces consistent redacted SQL' do + expect(first_redacted_query).to eq(second_redacted_query) + end + end + end +end |