exwiw 0.6.1 → 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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +13 -1
- data/lib/exwiw/adapter/mongodb_adapter.rb +5 -0
- data/lib/exwiw/adapter/mysql_adapter.rb +1 -1
- data/lib/exwiw/adapter/postgresql_adapter.rb +1 -1
- data/lib/exwiw/adapter/sqlite_adapter.rb +1 -1
- data/lib/exwiw/adapter.rb +11 -0
- data/lib/exwiw/config_file.rb +46 -0
- data/lib/exwiw/explain_runner.rb +16 -0
- data/lib/exwiw/runner.rb +19 -0
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +1 -0
- data/lib/tasks/exwiw.rake +14 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12765a8f130dec0055149beb7ba69b37c9b548758cfbd8dd807c1499a9fbf0b5
|
|
4
|
+
data.tar.gz: 9165cda22d6e4eb2ff97a6d386510a04f37d353e2b31ea4b6bada2fc1ad2adb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb9ad3e65e4b8756574b5c97fec28be38cc708604cbdaffcb772bd8958b183a067dffafa44078705ed00346be27a9aa81bd68a212b7bcecf635a84d6a7f86adc
|
|
7
|
+
data.tar.gz: fd47a8459abec5a56ab2c64c7cecc4ffa916608b2cf9278a68a2985defa18812b4f09b70a54b8d6ac498a305022644a0a0d923358a5a06143dedcb550aab7e84
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
11
|
+
## [0.6.2] - 2026-06-21
|
|
12
|
+
|
|
5
13
|
## [0.6.1] - 2026-06-20
|
|
6
14
|
|
|
7
15
|
## [0.6.0] - 2026-06-20
|
data/README.md
CHANGED
|
@@ -253,12 +253,18 @@ The config generator is provided as a Rake task.
|
|
|
253
253
|
bundle exec rake exwiw:schema:generate
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
|
|
256
|
+
The output directory is resolved in this order:
|
|
257
|
+
|
|
258
|
+
1. the `EXWIW_SCHEMA_DIR_PATH` environment variable, if set;
|
|
259
|
+
2. otherwise `schema_dir` from the config file (`exwiw.yml` / `exwiw.yaml` in the current directory), so the generator and the `exwiw` CLI share one location without repeating the path;
|
|
260
|
+
3. otherwise the `exwiw/schema` default.
|
|
257
261
|
|
|
258
262
|
```sh
|
|
259
263
|
EXWIW_SCHEMA_DIR_PATH=custom_directory bundle exec rake exwiw:schema:generate
|
|
260
264
|
```
|
|
261
265
|
|
|
266
|
+
As with the CLI, a relative `schema_dir` in the config file is resolved relative to the config file's own directory.
|
|
267
|
+
|
|
262
268
|
#### Tidying stale config (`schema:tidy`)
|
|
263
269
|
|
|
264
270
|
`schema:generate` adds and updates config files for the tables it finds, but it never deletes the config file of a table that has been dropped from the application. To reconcile the existing config against the current schema, run:
|
|
@@ -608,6 +614,12 @@ and you can use the column name with `{}` to replace the value with the column v
|
|
|
608
614
|
For example, Let assume we have the record which id is 1,
|
|
609
615
|
then "user{id}@example.com" will be replaced with "user1@example.com".
|
|
610
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
|
+
|
|
611
623
|
#### `raw_sql`
|
|
612
624
|
|
|
613
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|
|
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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Exwiw
|
|
6
|
+
# Minimal reader for the exwiw config YAML (exwiw.yml / exwiw.yaml).
|
|
7
|
+
#
|
|
8
|
+
# The CLI has its own, richer config handling (CLI#apply_config_file!); this
|
|
9
|
+
# module exists for contexts that have no CLI in scope — chiefly the
|
|
10
|
+
# `exwiw:schema:*` rake tasks, which otherwise only knew the
|
|
11
|
+
# EXWIW_SCHEMA_DIR_PATH env var and a hard-coded default. It deliberately
|
|
12
|
+
# reads only what those tasks need (schema_dir) and never aborts the process:
|
|
13
|
+
# an absent or unreadable config simply yields nil so the caller can fall back.
|
|
14
|
+
module ConfigFile
|
|
15
|
+
# Mirrors CLI::DEFAULT_CONFIG_PATHS; .yml wins when both are present.
|
|
16
|
+
DEFAULT_PATHS = %w[exwiw.yml exwiw.yaml].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# The `schema_dir` from the config file, expanded to an absolute path
|
|
21
|
+
# relative to the config file's own directory (matching the CLI). Returns
|
|
22
|
+
# nil when no config file is found, it cannot be parsed, or it does not set
|
|
23
|
+
# `schema_dir`. Pass an explicit `path` to read a specific file; otherwise
|
|
24
|
+
# the default paths are looked up in the current directory.
|
|
25
|
+
def schema_dir(path = nil)
|
|
26
|
+
path ||= DEFAULT_PATHS.map { |p| File.expand_path(p) }.find { |p| File.file?(p) }
|
|
27
|
+
return nil if path.nil? || !File.file?(path)
|
|
28
|
+
|
|
29
|
+
config =
|
|
30
|
+
begin
|
|
31
|
+
YAML.safe_load(File.read(path))
|
|
32
|
+
rescue Psych::SyntaxError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
return nil unless config.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
value = config["schema_dir"]
|
|
38
|
+
return nil if value.nil?
|
|
39
|
+
|
|
40
|
+
# Strip a trailing slash and resolve relative to the config file's
|
|
41
|
+
# directory, exactly as CLI#expand_dir does.
|
|
42
|
+
value = value.end_with?("/") ? value[0..-2] : value
|
|
43
|
+
File.expand_path(value, File.dirname(File.expand_path(path)))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/exwiw/explain_runner.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Exwiw
|
|
|
24
24
|
table_by_name = configs.each_with_object({}) { |config, hash| hash[config.name] = config }
|
|
25
25
|
|
|
26
26
|
target = table_by_name[@dump_target.table_name]
|
|
27
|
+
validate_target_exists!(target)
|
|
27
28
|
adapter.validate_as_dump_target!(target) if target
|
|
28
29
|
|
|
29
30
|
dumpable_configs = configs.select { |c| adapter.dumpable?(c) }
|
|
@@ -62,6 +63,21 @@ module Exwiw
|
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# Reject a `--target-table` (or `--target-collection`) absent from the loaded
|
|
67
|
+
# schema; mirrors Runner#validate_target_exists! so explain and export fail the
|
|
68
|
+
# same way on a typo. `target` is the looked-up config (nil when not found).
|
|
69
|
+
#
|
|
70
|
+
# TODO: same caveat as Runner#validate_target_exists! — this checks the schema
|
|
71
|
+
# (schema_dir JSON), not the live DB connection; verifying against the
|
|
72
|
+
# connection would need a table-exists capability on each adapter. revisit.
|
|
73
|
+
private def validate_target_exists!(target)
|
|
74
|
+
return if @dump_target.table_name.nil?
|
|
75
|
+
return unless target.nil?
|
|
76
|
+
|
|
77
|
+
raise ArgumentError,
|
|
78
|
+
"--target-table '#{@dump_target.table_name}' does not exist in the schema (#{@schema_dir})."
|
|
79
|
+
end
|
|
80
|
+
|
|
65
81
|
private def validate_ignored(configs)
|
|
66
82
|
ignored_names = configs.select { |c| c.ignore }.map(&:name).to_set
|
|
67
83
|
return if ignored_names.empty?
|
data/lib/exwiw/runner.rb
CHANGED
|
@@ -36,6 +36,7 @@ module Exwiw
|
|
|
36
36
|
table_by_name = configs.each_with_object({}) { |config, hash| hash[config.name] = config }
|
|
37
37
|
|
|
38
38
|
target = table_by_name[@dump_target.table_name]
|
|
39
|
+
validate_target_exists!(target)
|
|
39
40
|
adapter.validate_as_dump_target!(target) if target
|
|
40
41
|
|
|
41
42
|
dumpable_configs = configs.select { |c| adapter.dumpable?(c) }
|
|
@@ -211,6 +212,24 @@ module Exwiw
|
|
|
211
212
|
ignored_names.each { |n| @logger.info("Table '#{n}' is marked ignore:true (schema will be included, data extraction skipped)") }
|
|
212
213
|
end
|
|
213
214
|
|
|
215
|
+
# Reject a `--target-table` (or `--target-collection`) that does not match any
|
|
216
|
+
# table/collection in the loaded schema. Without this a typo'd target silently
|
|
217
|
+
# matched nothing and produced an empty dump with no indication of the mistake.
|
|
218
|
+
# `target` is the looked-up config (nil when not found); a nil dump target
|
|
219
|
+
# (dump-all / scope-column mode) is allowed through.
|
|
220
|
+
#
|
|
221
|
+
# TODO: this checks the loaded schema (schema_dir JSON), not the live DB
|
|
222
|
+
# connection — a table that exists in the database but has no schema config
|
|
223
|
+
# is still rejected here. We may instead want to verify existence against the
|
|
224
|
+
# connection (would need a table-exists capability on each adapter). revisit.
|
|
225
|
+
private def validate_target_exists!(target)
|
|
226
|
+
return if @dump_target.table_name.nil?
|
|
227
|
+
return unless target.nil?
|
|
228
|
+
|
|
229
|
+
raise ArgumentError,
|
|
230
|
+
"--target-table '#{@dump_target.table_name}' does not exist in the schema (#{@schema_dir})."
|
|
231
|
+
end
|
|
232
|
+
|
|
214
233
|
private def validate_rails_managed_target!(configs)
|
|
215
234
|
return if @dump_target.table_name.nil?
|
|
216
235
|
|
data/lib/exwiw/version.rb
CHANGED
data/lib/exwiw.rb
CHANGED
data/lib/tasks/exwiw.rake
CHANGED
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
namespace :exwiw do
|
|
4
4
|
namespace :schema do
|
|
5
|
+
# Output directory for the generated schema config. Precedence:
|
|
6
|
+
# 1. EXWIW_SCHEMA_DIR_PATH env var (explicit per-run override)
|
|
7
|
+
# 2. schema_dir in the exwiw config file (exwiw.yml/.yaml), so generating
|
|
8
|
+
# the schema and running the `exwiw` CLI agree on one location without
|
|
9
|
+
# repeating the path
|
|
10
|
+
# 3. the historical "exwiw/schema" default
|
|
11
|
+
# Resolved at task-run time (after `require "exwiw"` has loaded ConfigFile).
|
|
12
|
+
resolve_schema_dir = lambda do
|
|
13
|
+
ENV["EXWIW_SCHEMA_DIR_PATH"] || Exwiw::ConfigFile.schema_dir || "exwiw/schema"
|
|
14
|
+
end
|
|
15
|
+
|
|
5
16
|
desc "Generate schema from application"
|
|
6
17
|
task generate: :environment do
|
|
7
18
|
require "exwiw"
|
|
8
19
|
|
|
9
20
|
Exwiw::SchemaGenerator.from_rails_application(
|
|
10
|
-
output_dir:
|
|
21
|
+
output_dir: resolve_schema_dir.call,
|
|
11
22
|
).generate!
|
|
12
23
|
end
|
|
13
24
|
|
|
@@ -16,7 +27,7 @@ namespace :exwiw do
|
|
|
16
27
|
require "exwiw"
|
|
17
28
|
|
|
18
29
|
result = Exwiw::SchemaGenerator.from_rails_application(
|
|
19
|
-
output_dir:
|
|
30
|
+
output_dir: resolve_schema_dir.call,
|
|
20
31
|
).tidy!
|
|
21
32
|
|
|
22
33
|
if result.empty?
|
|
@@ -47,7 +58,7 @@ namespace :exwiw do
|
|
|
47
58
|
require "exwiw"
|
|
48
59
|
|
|
49
60
|
Exwiw::MongoidSchemaGenerator.from_rails_application(
|
|
50
|
-
output_dir:
|
|
61
|
+
output_dir: resolve_schema_dir.call,
|
|
51
62
|
skip_unsupported: ENV["EXWIW_SKIP_UNSUPPORTED"] == "1",
|
|
52
63
|
).generate!
|
|
53
64
|
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.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shia
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- lib/exwiw/after_insert_hook.rb
|
|
62
62
|
- lib/exwiw/belongs_to.rb
|
|
63
63
|
- lib/exwiw/cli.rb
|
|
64
|
+
- lib/exwiw/config_file.rb
|
|
64
65
|
- lib/exwiw/ddl_postprocessor.rb
|
|
65
66
|
- lib/exwiw/determine_table_processing_order.rb
|
|
66
67
|
- lib/exwiw/embedded_in.rb
|