exwiw 0.6.2 → 0.7.0

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: 2dc1d7b1052722433d6328889f3fba23233d09edfdf1db1d3c27d89598951cb1
4
- data.tar.gz: 7e0cdbe59e89ea5fb7ced4dce6e170a0e88d09715789ca1faea4f36bc2558a66
3
+ metadata.gz: 12765a8f130dec0055149beb7ba69b37c9b548758cfbd8dd807c1499a9fbf0b5
4
+ data.tar.gz: 9165cda22d6e4eb2ff97a6d386510a04f37d353e2b31ea4b6bada2fc1ad2adb7
5
5
  SHA512:
6
- metadata.gz: 5ab4c1f3c40d11c8d717d20639b3338c18df168880059c6c1e74d33e6f6bf9d9ce9f6921b9e8c613dc4aa24f8dfaf058017382763af45a888edc3c665e38373a
7
- data.tar.gz: ebddd4d938ef4369e41a87e4b57cde92c0cf534920459eafc6df7e5d0eeec2fc37810effe0ba4656a14890d75c73e2cb3c0b9f9f9f38e5bf9807df2ec156585e
6
+ metadata.gz: eb9ad3e65e4b8756574b5c97fec28be38cc708604cbdaffcb772bd8958b183a067dffafa44078705ed00346be27a9aa81bd68a212b7bcecf635a84d6a7f86adc
7
+ data.tar.gz: fd47a8459abec5a56ab2c64c7cecc4ffa916608b2cf9278a68a2985defa18812b4f09b70a54b8d6ac498a305022644a0a0d923358a5a06143dedcb550aab7e84
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.0] - 2026-06-23
6
+
7
+ ### Changed
8
+
9
+ - **`replace_with` now preserves NULL.** A NULL source value stays NULL instead of being replaced by the masked literal (all adapters: MySQL, PostgreSQL, SQLite, MongoDB). Previously masking a nullable column clobbered NULL into a non-NULL value, losing the "not set" signal in the dump and making `.nil?`/`.present?`-dependent code behave differently than against production. The SQL adapters now wrap the masking expression in `CASE WHEN <column> IS NOT NULL THEN <masked> ELSE NULL END`, and the MongoDB adapter skips masking a field whose value is nil or absent. This is a behavior change for **nullable** masked columns only — `NOT NULL` columns are unaffected, and an empty string is a real value that is still masked (only true NULL/absent is preserved). It removes the need for hand-written `raw_sql` `CASE` workarounds to keep NULLs.
10
+
5
11
  ## [0.6.2] - 2026-06-21
6
12
 
7
13
  ## [0.6.1] - 2026-06-20
data/README.md CHANGED
@@ -614,6 +614,12 @@ and you can use the column name with `{}` to replace the value with the column v
614
614
  For example, Let assume we have the record which id is 1,
615
615
  then "user{id}@example.com" will be replaced with "user1@example.com".
616
616
 
617
+ `replace_with` **preserves NULL**: a source value that is `NULL` (or, for MongoDB, an
618
+ absent field) is left as-is instead of being replaced by the masked literal, so the
619
+ "not set" signal survives into the dump. Only true `NULL`/absent is preserved — an empty
620
+ string is a real value and is still masked. Because of this you do not need to hand-write a
621
+ `raw_sql` `CASE WHEN ... IS NOT NULL ...` to keep NULLs.
622
+
617
623
  #### `raw_sql`
618
624
 
619
625
  It will used instead of the original value.
@@ -514,6 +514,11 @@ module Exwiw
514
514
  # pair, with all per-config lookups hoisted into the plan.
515
515
  private def apply_mask_plan!(doc, plan)
516
516
  plan.masked_fields.each do |name, segments|
517
+ # Preserve a NULL / absent source value instead of clobbering it into a
518
+ # masked literal. `doc[name].nil?` is true for both an explicit nil and
519
+ # an absent key, so an absent key is left absent (not created).
520
+ next if doc[name].nil?
521
+
517
522
  doc[name] = render_template(segments, doc)
518
523
  end
519
524
  plan.embedded.each do |child|
@@ -330,7 +330,7 @@ module Exwiw
330
330
  end
331
331
 
332
332
  replaced = parts.join(", ")
333
- "CONCAT(#{replaced})"
333
+ null_preserving(ast, column, "CONCAT(#{replaced})")
334
334
  else
335
335
  raise "Unreachable case: #{column.inspect}"
336
336
  end
@@ -456,7 +456,7 @@ module Exwiw
456
456
  end
457
457
 
458
458
  replaced = parts.join(", ")
459
- "CONCAT(#{replaced})"
459
+ null_preserving(ast, column, "CONCAT(#{replaced})")
460
460
  else
461
461
  raise "Unreachable case: #{column.inspect}"
462
462
  end
@@ -298,7 +298,7 @@ module Exwiw
298
298
  end
299
299
 
300
300
  replaced = parts.join(" || ")
301
- "(#{replaced})"
301
+ null_preserving(ast, column, "(#{replaced})")
302
302
  else
303
303
  raise "Unreachable case: #{column.inspect}"
304
304
  end
data/lib/exwiw/adapter.rb CHANGED
@@ -202,6 +202,17 @@ module Exwiw
202
202
  rescue => e
203
203
  "<unavailable: #{e.class}: #{e.message}>"
204
204
  end
205
+
206
+ # Wrap a masking expression so a NULL source value stays NULL instead of
207
+ # being clobbered into a non-NULL literal. The guard checks the masked
208
+ # column itself (`column.name`) — not any other column the template may
209
+ # reference — so only true NULL is preserved; an empty string is a real
210
+ # value and is still masked. The column reference uses the same
211
+ # `<from_table>.<col>` form as the Plain branch. Shared by the SQL
212
+ # adapters' #compile_column_name ReplaceWith handling.
213
+ private def null_preserving(ast, column, masked_expr)
214
+ "CASE WHEN #{ast.from_table_name}.#{column.name} IS NOT NULL THEN #{masked_expr} ELSE NULL END"
215
+ end
205
216
  end
206
217
 
207
218
  # @params [Exwiw::QueryAst] query_ast
data/lib/exwiw/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exwiw
4
- VERSION = "0.6.2"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exwiw
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia