exwiw 0.9.6 → 0.9.8
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 +17 -0
- data/README.md +132 -12
- data/docs/row-transform-masking-notes.md +156 -0
- data/lib/exwiw/adapter/mongodb_adapter.rb +118 -2
- data/lib/exwiw/determine_table_processing_order.rb +109 -31
- data/lib/exwiw/explain_runner.rb +13 -2
- data/lib/exwiw/fake_data.rb +21 -0
- data/lib/exwiw/mongodb_collection_config.rb +31 -0
- data/lib/exwiw/row_transformer.rb +243 -0
- data/lib/exwiw/runner.rb +36 -5
- data/lib/exwiw/strict_keys.rb +79 -0
- data/lib/exwiw/table_column.rb +5 -0
- data/lib/exwiw/table_config.rb +45 -0
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +3 -0
- metadata +5 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exwiw
|
|
4
|
+
# Raised when a schema config JSON carries a key that no declared attribute
|
|
5
|
+
# accepts. A subclass of ArgumentError so existing "invalid config" handling
|
|
6
|
+
# (and specs) that rescue ArgumentError keep working.
|
|
7
|
+
UnknownConfigKeyError = Class.new(ArgumentError)
|
|
8
|
+
|
|
9
|
+
# Strict key validation for the schema config JSON.
|
|
10
|
+
#
|
|
11
|
+
# Serdes deserialization is lenient: a key that matches no declared attribute
|
|
12
|
+
# is silently dropped. For hand-maintained schema configs that turns a typo
|
|
13
|
+
# (`reverse_scop`) — or a key another adapter supports but this one does not
|
|
14
|
+
# (`raw_sql` on a MongoDB field) — into a silent no-op: the config loads, the
|
|
15
|
+
# dump runs, and the requested masking/scoping simply never happens.
|
|
16
|
+
# {TableConfig.from} / {MongodbCollectionConfig.from} therefore validate the
|
|
17
|
+
# raw hash against the declared attributes before deserializing, recursing
|
|
18
|
+
# into nested config objects (belongs_tos, columns/fields, reverse_scope,
|
|
19
|
+
# embedded_in, replace_with_fake_data).
|
|
20
|
+
#
|
|
21
|
+
# Free-form annotations do not need an escape hatch: `comment` is a declared
|
|
22
|
+
# (documentation-only) attribute on the table/collection configs and their
|
|
23
|
+
# belongs_to/column/field entries, so it always passes.
|
|
24
|
+
module StrictKeys
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Validate `hash` (a raw config hash, string- or symbol-keyed) against the
|
|
28
|
+
# attributes declared on the Serdes class `klass`, recursing into nested
|
|
29
|
+
# Serdes-typed attributes. `owner` names the config for the error message
|
|
30
|
+
# (e.g. "table 'users'"); `path` locates a nested entry within it (e.g.
|
|
31
|
+
# "belongs_tos[0]"). No-op for non-Hash input — Serdes' own type errors
|
|
32
|
+
# cover malformed values.
|
|
33
|
+
def validate!(klass, hash, owner:, path: nil)
|
|
34
|
+
return unless hash.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
attributes = klass.__send__(:_serde_attrs)
|
|
37
|
+
rename_strategy = klass.__send__(:_serde_rename_strategy)
|
|
38
|
+
allowed = attributes.each_value.to_h { |attr| [attr.serialized_name(rename_strategy), attr] }
|
|
39
|
+
|
|
40
|
+
unknown = hash.keys.map(&:to_s) - allowed.keys
|
|
41
|
+
unless unknown.empty?
|
|
42
|
+
location = path ? " (in #{path})" : ""
|
|
43
|
+
raise UnknownConfigKeyError,
|
|
44
|
+
"Unknown key#{'s' if unknown.size > 1} #{unknown.map { |key| "'#{key}'" }.join(', ')} " \
|
|
45
|
+
"in #{owner}#{location}. Allowed keys: #{allowed.keys.sort.join(', ')}. " \
|
|
46
|
+
"Unknown keys are rejected because they would otherwise be silently ignored " \
|
|
47
|
+
"(hiding typos and keys this adapter does not support); remove or rename the key, " \
|
|
48
|
+
"or use `comment` for free-form notes."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
allowed.each do |key, attr|
|
|
52
|
+
nested_klass = serdes_class(attr.attr_type)
|
|
53
|
+
next if nested_klass.nil?
|
|
54
|
+
|
|
55
|
+
value = hash.key?(key) ? hash[key] : hash[key.to_sym]
|
|
56
|
+
nested_path = path ? "#{path}.#{key}" : key
|
|
57
|
+
case value
|
|
58
|
+
when Hash
|
|
59
|
+
validate!(nested_klass, value, owner: owner, path: nested_path)
|
|
60
|
+
when Array
|
|
61
|
+
value.each_with_index do |element, index|
|
|
62
|
+
validate!(nested_klass, element, owner: owner, path: "#{nested_path}[#{index}]") if element.is_a?(Hash)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The Serdes-including class a (possibly optional/array-wrapped) attribute
|
|
69
|
+
# type deserializes into, or nil for scalar types.
|
|
70
|
+
def serdes_class(type)
|
|
71
|
+
case type
|
|
72
|
+
when Serdes::OptionalType then serdes_class(type.base_type)
|
|
73
|
+
when Serdes::ArrayType then serdes_class(type.element_type)
|
|
74
|
+
when Serdes::ConcreteType then serdes_class(type.exact_type)
|
|
75
|
+
when Class then type if type.include?(Serdes)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/exwiw/table_column.rb
CHANGED
|
@@ -7,6 +7,11 @@ module Exwiw
|
|
|
7
7
|
attribute :name, String
|
|
8
8
|
attribute :replace_with, optional(String), skip_serializing_if_nil: true
|
|
9
9
|
attribute :raw_sql, optional(String), skip_serializing_if_nil: true
|
|
10
|
+
# Ruby-process-side masking modes, applied to the fetched rows by
|
|
11
|
+
# RowTransformer (SQL adapters only) — unlike replace_with/raw_sql, which
|
|
12
|
+
# compile into the SELECT and run in the database.
|
|
13
|
+
attribute :map, optional(String), skip_serializing_if_nil: true
|
|
14
|
+
attribute :replace_with_fake_data, Serdes::OptionalType.new(FakeData), skip_serializing_if_nil: true
|
|
10
15
|
# User-owned fields preserved across schema regeneration (see
|
|
11
16
|
# TableConfig#merge). `ignore:true` drops the column from extraction (SELECT /
|
|
12
17
|
# INSERT) once the config is loaded (see TableConfig#reject_ignored_members!).
|
data/lib/exwiw/table_config.rb
CHANGED
|
@@ -48,6 +48,15 @@ module Exwiw
|
|
|
48
48
|
attribute :reverse_scope, Serdes::OptionalType.new(ReverseScope), skip_serializing_if_nil: true
|
|
49
49
|
|
|
50
50
|
def self.from(hash)
|
|
51
|
+
# Reject unknown keys before deserializing: Serdes silently drops them,
|
|
52
|
+
# which would turn a typo'd or unsupported key into a silent no-op (see
|
|
53
|
+
# Exwiw::StrictKeys). `comment` is a declared attribute here and on the
|
|
54
|
+
# nested belongs_to/column entries, so free-form notes stay accepted.
|
|
55
|
+
if hash.is_a?(Hash)
|
|
56
|
+
table_name = hash["name"] || hash[:name]
|
|
57
|
+
StrictKeys.validate!(self, hash, owner: "table '#{table_name}'")
|
|
58
|
+
end
|
|
59
|
+
|
|
51
60
|
config = super
|
|
52
61
|
config.send(:validate_after_load!)
|
|
53
62
|
config
|
|
@@ -219,6 +228,42 @@ module Exwiw
|
|
|
219
228
|
if primary_key.nil? && !ignore
|
|
220
229
|
raise ArgumentError, "Table '#{name}' requires primary_key."
|
|
221
230
|
end
|
|
231
|
+
|
|
232
|
+
columns.each { |column| validate_ruby_side_masking!(column) }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The Ruby-side masking modes (`map` / `replace_with_fake_data`) are strictly
|
|
237
|
+
# exclusive — with each other and with the SQL-side modes. Only the new keys
|
|
238
|
+
# are restricted: the legacy raw_sql > replace_with precedence stays lenient.
|
|
239
|
+
# Deliberately static (no faker require, no proc eval) so schema
|
|
240
|
+
# regeneration never executes config Ruby; the seed column and the map proc
|
|
241
|
+
# are resolved at dump time by RowTransformer.build.
|
|
242
|
+
private def validate_ruby_side_masking!(column)
|
|
243
|
+
ruby_side = [column.map && "map", column.replace_with_fake_data && "replace_with_fake_data"].compact
|
|
244
|
+
return if ruby_side.empty?
|
|
245
|
+
|
|
246
|
+
sql_side = [column.raw_sql && "raw_sql", column.replace_with && "replace_with"].compact
|
|
247
|
+
if ruby_side.size > 1 || sql_side.any?
|
|
248
|
+
raise ArgumentError,
|
|
249
|
+
"Table '#{name}' column '#{column.name}': #{(ruby_side + sql_side).join('/')} cannot be combined; " \
|
|
250
|
+
"use at most one of map/replace_with_fake_data, without raw_sql/replace_with."
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
fake_data = column.replace_with_fake_data
|
|
254
|
+
return unless fake_data
|
|
255
|
+
|
|
256
|
+
unless RowTransformer::FAKE_TYPES.key?(fake_data.type)
|
|
257
|
+
raise ArgumentError,
|
|
258
|
+
"Table '#{name}' column '#{column.name}': unknown replace_with_fake_data type '#{fake_data.type}' " \
|
|
259
|
+
"(supported: #{RowTransformer::FAKE_TYPES.keys.join(', ')})."
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
seed_column = fake_data.seed.delete_prefix("#{name}.")
|
|
263
|
+
if seed_column.include?(".") || columns.none? { |c| c.name == seed_column }
|
|
264
|
+
raise ArgumentError,
|
|
265
|
+
"Table '#{name}' column '#{column.name}': replace_with_fake_data seed '#{fake_data.seed}' " \
|
|
266
|
+
"does not name a column of this table (use 'column' or '#{name}.column')."
|
|
222
267
|
end
|
|
223
268
|
end
|
|
224
269
|
|
data/lib/exwiw/version.rb
CHANGED
data/lib/exwiw.rb
CHANGED
|
@@ -7,7 +7,9 @@ require "serdes"
|
|
|
7
7
|
|
|
8
8
|
require_relative "exwiw/ext_json"
|
|
9
9
|
require_relative "exwiw/config_file"
|
|
10
|
+
require_relative "exwiw/strict_keys"
|
|
10
11
|
require_relative "exwiw/belongs_to"
|
|
12
|
+
require_relative "exwiw/fake_data"
|
|
11
13
|
require_relative "exwiw/table_column"
|
|
12
14
|
require_relative "exwiw/reverse_scope"
|
|
13
15
|
require_relative "exwiw/table_config"
|
|
@@ -28,6 +30,7 @@ require_relative "exwiw/mongodb_parallel_dumper"
|
|
|
28
30
|
require_relative "exwiw/mongo_query"
|
|
29
31
|
require_relative "exwiw/query_ast"
|
|
30
32
|
require_relative "exwiw/query_ast_builder"
|
|
33
|
+
require_relative "exwiw/row_transformer"
|
|
31
34
|
require_relative "exwiw/after_insert_hook"
|
|
32
35
|
require_relative "exwiw/runner"
|
|
33
36
|
require_relative "exwiw/explain_runner"
|
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.9.
|
|
4
|
+
version: 0.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shia
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- docs/plans/2026-05-29-rails-managed-tables.md
|
|
48
48
|
- docs/plans/2026-05-31-ids-column-for-sql-adapters.md
|
|
49
49
|
- docs/plans/2026-06-19-mongodb-export-remove-parallelism-native-ext.md
|
|
50
|
+
- docs/row-transform-masking-notes.md
|
|
50
51
|
- docs/scope-column-redesign.md
|
|
51
52
|
- docs/sql-dump-optimization-notes.md
|
|
52
53
|
- exe/exwiw
|
|
@@ -69,6 +70,7 @@ files:
|
|
|
69
70
|
- lib/exwiw/embedded_in.rb
|
|
70
71
|
- lib/exwiw/explain_runner.rb
|
|
71
72
|
- lib/exwiw/ext_json.rb
|
|
73
|
+
- lib/exwiw/fake_data.rb
|
|
72
74
|
- lib/exwiw/mongo_query.rb
|
|
73
75
|
- lib/exwiw/mongodb_collection_config.rb
|
|
74
76
|
- lib/exwiw/mongodb_field.rb
|
|
@@ -79,8 +81,10 @@ files:
|
|
|
79
81
|
- lib/exwiw/query_ast_builder.rb
|
|
80
82
|
- lib/exwiw/railtie.rb
|
|
81
83
|
- lib/exwiw/reverse_scope.rb
|
|
84
|
+
- lib/exwiw/row_transformer.rb
|
|
82
85
|
- lib/exwiw/runner.rb
|
|
83
86
|
- lib/exwiw/schema_generator.rb
|
|
87
|
+
- lib/exwiw/strict_keys.rb
|
|
84
88
|
- lib/exwiw/table_column.rb
|
|
85
89
|
- lib/exwiw/table_config.rb
|
|
86
90
|
- lib/exwiw/version.rb
|