database_consistency 3.0.11 → 3.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af9d610b968b1e10caed92c35cd94d724a93e75af2bfa3b14ac3d9f5af4592d7
4
- data.tar.gz: a7d5e9c0fd7e1ca330da9104dc61a11f39d09b433b0296b2d63c16e0d72d9a56
3
+ metadata.gz: cf61dd8acf18a8e18b559df07d481e8503576cd228ec74190aa54b2a5b11e200
4
+ data.tar.gz: 4c2ffed1815099f0c2102043b976a85236017b6c4d92287741a48459824a047c
5
5
  SHA512:
6
- metadata.gz: 14fdf20cb9ec117660bf497981b548df0b16259f0206d89a62d088f63c87eca9f361dfc3ff3dde02b00af34ae2bc12bc6687f425a2f423b0b2d919e3a13f9796
7
- data.tar.gz: e23ea79b4d0883e5bc6117e7ebc02236f1ec47a6175725390b1c596c7b6fb509755d2285819faec994ccf5434fe2b5eaf8c31f74f0f89886e51f613fd8486225
6
+ metadata.gz: b04e984adeec1807d37f83b6826d0e4499e95c1bc8b3042b9a078e348de499d1df1d1b2437972eb2899a6716da8f4e498d946629570e761ec3ef59969254ffac
7
+ data.tar.gz: c100ad7001fe64ce78b1fed323b0e1c389b4bacfb899a26a215731ca4327f6b026828063ea48ac3a494958c8b59916a94b88577c16d342c6205b69d843b11769
@@ -151,7 +151,7 @@ module DatabaseConsistency
151
151
  where_part = sql.split(/\bWHERE\b/i, 2).last
152
152
  return nil unless where_part
153
153
 
154
- normalize_condition_sql(where_part.gsub("#{model.quoted_table_name}.", '').gsub('"', ''))
154
+ normalize_condition_sql(where_part.gsub("#{model.quoted_table_name}.", ''))
155
155
  rescue StandardError
156
156
  nil
157
157
  end
@@ -188,31 +188,68 @@ module DatabaseConsistency
188
188
  validator_where.casecmp?(normalized_where)
189
189
  end
190
190
 
191
+ # Prefix used to mask string literals while regex normalization runs, so
192
+ # patterns that strip casts or unwrap parentheses never see the inside of a
193
+ # literal value. Angle brackets are used so the placeholder cannot be mistaken
194
+ # for a column name by the boolean-predicate normalizer.
195
+ LITERAL_PLACEHOLDER = '__DATABASE_CONSISTENCY_LITERAL<%<index>d>__'
196
+
191
197
  # Normalizes SQL predicates into a canonical form so semantically equivalent
192
198
  # Rails validators and database partial indexes can be compared safely.
193
199
  def normalize_condition_sql(sql)
194
- sql
195
- .to_s
196
- .then { |value| strip_outer_parentheses(value) }
197
- .then { |value| normalize_sql(value) }
200
+ masked_sql, literals = sql.to_s
201
+ .then { |value| strip_outer_parentheses(value) }
202
+ .then { |value| normalize_sql_pre_mask(value) }
203
+ .then { |value| mask_condition_literals(value) }
204
+
205
+ normalize_masked_condition_sql(masked_sql, literals)
206
+ end
207
+
208
+ # Finishes normalization after string literals have been masked: runs the
209
+ # regex-based transforms that must not see inside literals, restores the
210
+ # literals, then applies the final clean-ups.
211
+ def normalize_masked_condition_sql(masked_sql, literals)
212
+ masked_sql
213
+ .then { |value| normalize_sql_post_mask(value) }
198
214
  .then { |value| normalize_boolean_predicates(value) }
199
215
  .then { |value| normalize_array_any_predicates(value) }
216
+ .then { |value| unmask_condition_literals(value, literals) }
200
217
  .then { |value| normalize_negated_blank_or_nil_predicates(value) }
201
218
  .then { |value| sort_and_clauses(value) }
219
+ .then { |value| value.gsub(/\s+/, ' ').strip }
220
+ end
221
+
222
+ # Masks non-empty string literals so later regexes cannot rewrite their
223
+ # contents. Empty literals are left untouched because negated-blank
224
+ # normalization relies on them.
225
+ def mask_condition_literals(sql)
226
+ literals = []
227
+ masked_sql = sql.gsub(/'(?:[^']|'')*'/) do |match|
228
+ if match == "''"
229
+ match
230
+ else
231
+ literals << match
232
+ format(LITERAL_PLACEHOLDER, index: literals.length - 1)
233
+ end
234
+ end
235
+ [masked_sql, literals]
202
236
  end
203
237
 
204
- # Applies lightweight SQL normalization without changing the logical meaning.
205
- def normalize_sql(sql)
206
- # `/::\w+/` removes PostgreSQL casts like `column::text`.
207
- normalized_sql = sql.gsub(/::\w+/, '')
208
- # `/\(([a-z_][\w.]*)\)/i` unwraps a bare identifier surrounded by
209
- # parentheses, e.g. `(internal_name)` -> `internal_name`.
210
- normalized_sql = normalized_sql.gsub(/\(([a-z_][\w.]*)\)/i, '\1')
238
+ # Restores literals in the order they were masked. Uses a block replacement
239
+ # so backslashes inside the literal are not interpreted as regexp backrefs.
240
+ def unmask_condition_literals(sql, literals)
241
+ literals.each_with_index do |literal, index|
242
+ sql = sql.sub(format(LITERAL_PLACEHOLDER, index: index)) { literal }
243
+ end
244
+ sql
245
+ end
246
+
247
+ # Normalizations that must run before string literals are masked.
248
+ def normalize_sql_pre_mask(sql)
249
+ normalized_sql = sql.dup
211
250
  # `/\bTRUE\b/i` and `/\bFALSE\b/i` normalize boolean literals to `1` / `0`
212
251
  # so they match SQL generated by Active Record on some adapters.
213
252
  normalized_sql = normalized_sql.gsub(/\bTRUE\b/i, '1').gsub(/\bFALSE\b/i, '0')
214
- # `/\s*<>\s*/` rewrites the SQL inequality operator `<>` to `!=`.
215
- normalized_sql = normalized_sql.gsub(/\s*<>\s*/, ' != ')
216
253
  # `/\bIS\s+NOT\s+NULL\b/i` normalizes `IS NOT NULL` spacing and casing.
217
254
  normalized_sql = normalized_sql.gsub(/\bIS\s+NOT\s+NULL\b/i, ' IS NOT NULL')
218
255
  # `/\bIS\s+NULL\b/i` normalizes `IS NULL` spacing and casing.
@@ -220,9 +257,22 @@ module DatabaseConsistency
220
257
  # `/ = 't'/` and `/ = 'f'/` normalize PostgreSQL boolean literals stored
221
258
  # as `'t'` / `'f'` inside comparisons.
222
259
  normalized_sql = normalized_sql.gsub(/ = 't'/, ' = 1').gsub(/ = 'f'/, ' = 0')
223
- # `/\s+/` collapses any run of whitespace to a single space.
224
- normalized_sql = normalized_sql.gsub(/\s+/, ' ')
225
- normalized_sql.strip
260
+ normalized_sql.gsub(/\s+/, ' ').strip
261
+ end
262
+
263
+ # Normalizations that run while string literals are masked.
264
+ def normalize_sql_post_mask(sql)
265
+ # Strips quoted identifiers (double quotes on PostgreSQL/SQLite,
266
+ # backticks on MySQL) so the same column normalizes across adapters.
267
+ normalized_sql = sql.gsub(/["`]/, '')
268
+ # `/::\w+/` removes PostgreSQL casts like `column::text`.
269
+ normalized_sql = normalized_sql.gsub(/::\w+/, '')
270
+ # `/\(([a-z_][\w.]*)\)/i` unwraps a bare identifier surrounded by
271
+ # parentheses, e.g. `(internal_name)` -> `internal_name`.
272
+ normalized_sql = normalized_sql.gsub(/\(([a-z_][\w.]*)\)/i, '\1')
273
+ # `/\s*<>\s*/` rewrites the SQL inequality operator `<>` to `!=`.
274
+ normalized_sql = normalized_sql.gsub(/\s*<>\s*/, ' != ')
275
+ normalized_sql.gsub(/\s+/, ' ').strip
226
276
  end
227
277
 
228
278
  # Repeatedly removes one wrapping layer of parentheses when the whole SQL
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatabaseConsistency
4
- VERSION = '3.0.11'
4
+ VERSION = '3.0.12'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_consistency
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.11
4
+ version: 3.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord