Skip to content
Merged
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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# SqlQuery change log

## 0.7.5 / Unreleased
## 1.0.0 / Unreleased

* [Added] Configurable SQL comment removal feature with `remove_comments` and `remove_comments_from` configuration options. Supports selective removal of single-line (`--`) and multi-line (`/* */`) comments while preserving comments within quoted strings (single, double, and PostgreSQL dollar quotes). Addresses https://github.com/sufleR/sql_query/issues/20

* [Deprecated]

* [Removed]

* [Fixed]
* [Fixed] Whitespace normalization now properly supports multiline ERB blocks. Previously, `prepared_for_logs` would corrupt SQL templates containing multiline ERB code blocks (e.g., `<% ... %>`) by normalizing whitespace before ERB processing. The new `WhitespaceNormalizer` class renders ERB first, then intelligently collapses whitespace while preserving content within SQL quoted strings (single quotes, double quotes, and escaped quotes).

## 0.7.4 / 2024-04-20

Expand Down
9 changes: 7 additions & 2 deletions lib/sql_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'erb'
require_relative 'sql_query/config'
require_relative 'sql_query/comment_remover'
require_relative 'sql_query/whitespace_normalizer'

class SqlQuery
attr_reader :connection
Expand Down Expand Up @@ -71,8 +72,12 @@ def self.configure

def prepare_query(for_logs)
query_template = File.read(file_path)
query_template = query_template.gsub(/(\n|\s)+/, ' ') if for_logs
ERB.new(query_template).result(binding)
rendered_sql = ERB.new(query_template).result(binding)

return rendered_sql unless for_logs

# Normalize whitespace while preserving quoted strings
WhitespaceNormalizer.new.normalize(rendered_sql)
end

def split_to_path_and_name(file)
Expand Down
111 changes: 111 additions & 0 deletions lib/sql_query/whitespace_normalizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# frozen_string_literal: true

class SqlQuery
# Service class responsible for normalizing whitespace in SQL queries
# while preserving content within quoted strings.
#
# This class collapses multiple whitespace characters (spaces, tabs, newlines)
# into single spaces, except when they appear within SQL string literals.
#
# @example
# normalizer = WhitespaceNormalizer.new
# sql = "SELECT *\n FROM users\n WHERE name = ' John '"
# normalizer.normalize(sql)
# # => "SELECT * FROM users WHERE name = ' John '"
class WhitespaceNormalizer
# Normalizes whitespace in the given SQL string
#
# @param sql [String] the SQL string to normalize
# @return [String] the normalized SQL string
def normalize(sql)
state = { in_single: false, in_double: false, prev_space: false }
result = []
i = 0

i = process_character(sql, i, result, state) while i < sql.length

result.join
end

private

# rubocop:disable Metrics/MethodLength
def process_character(sql, index, result, state)
char = sql[index]

if single_quote?(char, state)
process_single_quote(sql, index, result, state)
elsif double_quote?(char, state)
process_double_quote(sql, index, result, state)
elsif whitespace?(char)
process_whitespace(char, result, state)
index + 1
else
process_regular_char(char, result, state)
index + 1
end
end

def single_quote?(char, state)
char == "'" && !state[:in_double]
end

def double_quote?(char, state)
char == '"' && !state[:in_single]
end

def whitespace?(char)
char =~ /\s/
end
# rubocop:enable Metrics/MethodLength

# rubocop:disable Metrics/AbcSize
def process_single_quote(sql, index, result, state)
if state[:in_single] && index + 1 < sql.length && sql[index + 1] == "'"
# Doubled quote (escape) - add both
result << sql[index] << sql[index + 1]
state[:prev_space] = false
index + 2
else
# Normal quote - toggle state
state[:in_single] = !state[:in_single]
result << sql[index]
state[:prev_space] = false
index + 1
end
end

def process_double_quote(sql, index, result, state)
if state[:in_double] && index + 1 < sql.length && sql[index + 1] == '"'
# Doubled quote (escape) - add both
result << sql[index] << sql[index + 1]
state[:prev_space] = false
index + 2
else
# Normal quote - toggle state
state[:in_double] = !state[:in_double]
result << sql[index]
state[:prev_space] = false
index + 1
end
end

def process_whitespace(char, result, state)
if state[:in_single] || state[:in_double]
# Inside quotes: preserve whitespace
result << char
state[:prev_space] = false
elsif !state[:prev_space]
# Outside quotes: collapse to single space
result << ' '
state[:prev_space] = true
end
end

def process_regular_char(char, result, state)
result << char
state[:prev_space] = false
end
# rubocop:enable Metrics/AbcSize
end
end
9 changes: 9 additions & 0 deletions spec/sql_queries/multiline_erb.sql.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%
field1 = @field1 || 'default1'
field2 = @field2 || 'default2'
%>
SELECT
<%= quote field1 %> as field1,
<%= quote field2 %> as field2
FROM players
WHERE email = <%= quote @email %>
17 changes: 17 additions & 0 deletions spec/sql_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ class Model < ActiveRecord::Base
.to eq("SELECT * FROM players WHERE email = ' e@mail.dev ' ")
end
end

context 'when template has multiline ERB blocks' do
let(:file_name) { :multiline_erb }
let(:options) { { email: 'test@dev.com', field1: 'val1', field2: 'val2' } }
let(:query) { described_class.new(file_name, options) }

it 'processes multiline ERB correctly without syntax errors' do
expect { query.prepared_for_logs }.not_to raise_error
end

it 'collapses SQL whitespace while preserving ERB processing' do
result = query.prepared_for_logs
expect(result).to include("'val1' as field1")
expect(result).to include("'val2' as field2")
expect(result).not_to include("\n") # All newlines should be collapsed
end
end
end

describe 'comment removal integration' do
Expand Down