database_consistency 3.0.13 → 3.0.14

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: 30c161f165c612d3196799c754ef057f0236c1bdcfd1bbd87d3c2fc0a2f28bc8
4
- data.tar.gz: 1196509cb852b3a5ded62f8866b19f34ab6d9abc80af89d0806b08a25b779b56
3
+ metadata.gz: c0599ad1c39057b6a4ec4bb2c60938454a92296906258cda66b5b9f47081447e
4
+ data.tar.gz: 77d57bb76d65535e3dea3dfa88d8b5efcce14a230682ee472fd26a1f605c619a
5
5
  SHA512:
6
- metadata.gz: e4ba6f89c64740f04fa2a091fd5d137d4295b3884a9a24a524902e46ab20e80d5b7a76db77de59d1e8b214ef656724da103154169bbc99dada06eb6e1f150a26
7
- data.tar.gz: 618299c5d08832c05120596316c919526ee72b6646272b10dfa624d98a47021931a5f3baa439906305ec615cc3ed7e2f31e55c963891355ffe2fbf244b07c622
6
+ metadata.gz: 9cffcfa84c2731c584788352910c712f9947cc4fcdc73f24bb68128889f6ec0674accdef8d2390d113fb4f16c7e37e5c859dd09fe649d75b2be04b4964fc10c3
7
+ data.tar.gz: 3fc92238031f7e9e3611179a5fcad66b8105374eaa799416ae426248a9f88b7baafd61642e98a2eb03d3db0d591cf241d7515388f309b058dca2c20fd04d783d
@@ -199,6 +199,22 @@ module DatabaseConsistency
199
199
  # rather than the end of it.
200
200
  CONDITION_LITERAL = /'(?:[^']|'')*'/.freeze
201
201
 
202
+ # Matches a masked literal, so steps that run on masked SQL can step over
203
+ # the `<` and `>` in the placeholder.
204
+ MASKED_LITERAL = Regexp.new(
205
+ Regexp.escape(LITERAL_PLACEHOLDER).sub('%<index>d') { '\d+' }
206
+ ).freeze
207
+
208
+ # Matches an operator together with whatever spaces were written around it.
209
+ # `+` and `-` are left out: either can be a sign as well as an operator, and
210
+ # telling the two apart takes a parser. `->` and `->>` are the exception,
211
+ # since neither can be read as a sign.
212
+ CONDITION_OPERATOR = %r{\s*(->>?|[<>=!~@#%^&|?*/]+)\s*}.freeze
213
+
214
+ # Matches either of the two, so the spacing step can find operators while
215
+ # passing over the placeholders.
216
+ MASKED_LITERAL_OR_OPERATOR = Regexp.union(MASKED_LITERAL, CONDITION_OPERATOR).freeze
217
+
202
218
  # Matches a number PostgreSQL had to quote in order to coerce it, together
203
219
  # with the cast that says it is a number rather than a string. `::text` is
204
220
  # deliberately absent from the list so a genuine string keeps its quotes.
@@ -244,7 +260,9 @@ module DatabaseConsistency
244
260
  IN_LIST_OPENING = /(?<!\bIN\s)/i.freeze
245
261
 
246
262
  # Matches a bare identifier wrapped in parentheses, e.g. `(internal_name)`.
247
- WRAPPED_IDENTIFIER = /#{IN_LIST_OPENING}\(([a-z_][\w.]*)\)/i.freeze
263
+ # The lookbehind keeps the argument list of a call such as `lower(name)`
264
+ # intact.
265
+ WRAPPED_IDENTIFIER = /(?<![\w.])#{IN_LIST_OPENING}\(([a-z_][\w.]*)\)/i.freeze
248
266
 
249
267
  # Matches a parenthesized numeric literal, e.g. `(0)` or `(0.001)`, which is
250
268
  # what a cast such as `(0)::numeric` leaves behind once the cast is gone.
@@ -262,20 +280,26 @@ module DatabaseConsistency
262
280
 
263
281
  # Matches a bare negated boolean predicate such as `NOT archived`, in the
264
282
  # three places one can stand: at the start of an expression, after `AND` or
265
- # `OR`, or after an opening parenthesis.
283
+ # `OR`, or after an opening parenthesis. The whitespace before whatever
284
+ # follows sits inside the lookahead, so the match leaves it in place instead
285
+ # of consuming it and fusing the next `AND` / `OR` to the rewritten
286
+ # predicate. The lookbehind keeps a call's own parenthesis out of the
287
+ # boolean positions, so the argument of `lower(...)` is not read as a
288
+ # predicate of its own.
266
289
  NEGATED_BOOLEAN_PREDICATE = /
267
- (^ | (?: \bAND\b | \bOR\b | \( ))
268
- \s* NOT \s+ ([a-z_][\w.]*) \s*
269
- (?= $ | (?: \bAND\b | \bOR\b | \) ))
290
+ (^ | (?: \bAND\b | \bOR\b | (?<![\w.]) \( ))
291
+ \s* NOT \s+ ([a-z_][\w.]*)
292
+ (?= \s* (?: $ | \bAND\b | \bOR\b | \) ))
270
293
  /xi.freeze
271
294
 
272
295
  # Matches a bare boolean predicate such as `most_recent` in those same three
273
- # places. It runs after the negated form so that `NOT archived` is already
274
- # gone and cannot be read as the predicate `archived`.
296
+ # places, with the same lookahead and lookbehind. It runs after the negated
297
+ # form so that `NOT archived` is already gone and cannot be read as the
298
+ # predicate `archived`.
275
299
  BARE_BOOLEAN_PREDICATE = /
276
- (^ | (?: \bAND\b | \bOR\b | \( ))
277
- \s* ([a-z_][\w.]*) \s*
278
- (?= $ | (?: \bAND\b | \bOR\b | \) ))
300
+ (^ | (?: \bAND\b | \bOR\b | (?<![\w.]) \( ))
301
+ \s* ([a-z_][\w.]*)
302
+ (?= \s* (?: $ | \bAND\b | \bOR\b | \) ))
279
303
  /xi.freeze
280
304
 
281
305
  # Matches `column = ANY (ARRAY[...])` or `column != ALL ((ARRAY[...]))`,
@@ -442,11 +466,23 @@ module DatabaseConsistency
442
466
  "#{sign}#{expanded}".sub(/(\.\d*?)0+\z/, '\1').chomp('.')
443
467
  end
444
468
 
469
+ # Gives every operator a space either side, every comma a space after it
470
+ # and no parenthesis a space on its inside, which is how PostgreSQL writes
471
+ # an indexdef however the index was typed.
472
+ def normalize_operator_spacing(sql)
473
+ spaced_sql = sql.gsub(MASKED_LITERAL_OR_OPERATOR) do |match|
474
+ match.match?(MASKED_LITERAL) ? match : " #{match.strip} "
475
+ end
476
+ spaced_sql = spaced_sql.gsub(/\s*,\s*/, ', ')
477
+ spaced_sql.gsub(/\(\s+/, '(').gsub(/\s+\)/, ')')
478
+ end
479
+
445
480
  # Rewrites the spellings that differ between adapters, or between what an
446
481
  # adapter stores and what Active Record writes: quoted identifiers, casts,
447
- # exponent notation, the spacing of an `IN` list, the parentheses PostgreSQL
448
- # adds around a cast operand and the `<>` it writes for inequality. Literals are masked throughout, so
449
- # none of it reaches the inside of a value.
482
+ # exponent notation, the spacing of an `IN` list, of operators and of
483
+ # commas, the parentheses PostgreSQL adds around a cast operand and the `<>`
484
+ # it writes for inequality. Literals are masked throughout, so none of it
485
+ # reaches the inside of a value.
450
486
  def normalize_adapter_syntax(sql)
451
487
  # Strips quoted identifiers (double quotes on PostgreSQL/SQLite,
452
488
  # backticks on MySQL) so the same column normalizes across adapters.
@@ -457,9 +493,11 @@ module DatabaseConsistency
457
493
  # reach the same string and the list is recognisable to the unwrappers
458
494
  # below. `\b` keeps a call such as `min(1)` out of it.
459
495
  normalized_sql = normalized_sql.gsub(/\bIN\s*\(/i, 'IN (')
496
+ normalized_sql = normalize_operator_spacing(normalized_sql)
460
497
  normalized_sql = unwrap_redundant_parentheses(normalized_sql)
461
- # `/\s*<>\s*/` rewrites the SQL inequality operator `<>` to `!=`.
462
- normalized_sql = normalized_sql.gsub(/\s*<>\s*/, ' != ')
498
+ # Rewrites the SQL inequality operator `<>` to `!=`; the spacing step
499
+ # has already given it a space either side.
500
+ normalized_sql = normalized_sql.gsub('<>', '!=')
463
501
  normalized_sql.gsub(/\s+/, ' ').strip
464
502
  end
465
503
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatabaseConsistency
4
- VERSION = '3.0.13'
4
+ VERSION = '3.0.14'
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.13
4
+ version: 3.0.14
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-09-22 00:00:00.000000000 Z
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord