exwiw 0.9.20 → 0.9.22
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 +32 -0
- data/README.md +215 -3
- data/docs/mongodb.md +13 -0
- data/lib/exwiw/adapter/mongodb_adapter.rb +7 -4
- data/lib/exwiw/adapter/mysql_adapter.rb +4 -7
- data/lib/exwiw/adapter/postgresql_adapter.rb +4 -7
- data/lib/exwiw/adapter/sqlite_adapter.rb +4 -7
- data/lib/exwiw/adapter.rb +27 -0
- data/lib/exwiw/cli.rb +190 -3
- data/lib/exwiw/db_introspector/mysql_introspector.rb +160 -0
- data/lib/exwiw/db_introspector/postgresql_introspector.rb +215 -0
- data/lib/exwiw/db_introspector.rb +166 -0
- data/lib/exwiw/db_schema_generator.rb +298 -0
- data/lib/exwiw/default_mask.rb +104 -0
- data/lib/exwiw/mask_value.rb +30 -0
- data/lib/exwiw/mongodb_collection_config.rb +22 -10
- data/lib/exwiw/mongodb_field.rb +5 -1
- data/lib/exwiw/mongoid_schema_generator.rb +368 -36
- data/lib/exwiw/query_ast.rb +1 -1
- data/lib/exwiw/schema_check.rb +167 -0
- data/lib/exwiw/schema_generator.rb +64 -5
- data/lib/exwiw/table_column.rb +7 -1
- data/lib/exwiw/table_config.rb +2 -2
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +7 -0
- data/lib/tasks/exwiw.rake +61 -0
- metadata +8 -1
|
@@ -43,14 +43,20 @@ module Exwiw
|
|
|
43
43
|
# belongs_to to it.
|
|
44
44
|
ACTIVE_STORAGE_VARIANT_RECORDS_TABLE = "active_storage_variant_records"
|
|
45
45
|
|
|
46
|
-
def self.from_rails_application(output_dir:)
|
|
46
|
+
def self.from_rails_application(output_dir:, safe_new_columns: true)
|
|
47
47
|
Rails.application.eager_load!
|
|
48
|
-
new(models: ActiveRecord::Base.descendants, output_dir: output_dir)
|
|
48
|
+
new(models: ActiveRecord::Base.descendants, output_dir: output_dir, safe_new_columns: safe_new_columns)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# `safe_new_columns` (the default) emits every column masked — as far as its
|
|
52
|
+
# type allows, see DefaultMask — and flagged `needs_mask_decision: true`.
|
|
53
|
+
# #merge lets an existing entry win, so in practice only columns a migration
|
|
54
|
+
# has just added keep that treatment. Pass false to bootstrap a config, where
|
|
55
|
+
# every column is new and flagging all of them at once is noise.
|
|
56
|
+
def initialize(models:, output_dir:, safe_new_columns: true)
|
|
52
57
|
@models = models
|
|
53
58
|
@output_dir = output_dir
|
|
59
|
+
@safe_new_columns = safe_new_columns
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
def generate!
|
|
@@ -239,11 +245,12 @@ module Exwiw
|
|
|
239
245
|
columns: representative.column_names.map { |name| { name: name } },
|
|
240
246
|
)
|
|
241
247
|
else
|
|
248
|
+
belongs_tos = aggregate_belongs_tos(model_group)
|
|
242
249
|
TableConfig.from_symbol_keys(
|
|
243
250
|
name: table_name,
|
|
244
251
|
primary_key: primary_key,
|
|
245
|
-
belongs_tos:
|
|
246
|
-
columns: representative
|
|
252
|
+
belongs_tos: belongs_tos,
|
|
253
|
+
columns: build_columns(representative, primary_key, belongs_tos, conn),
|
|
247
254
|
)
|
|
248
255
|
end
|
|
249
256
|
end
|
|
@@ -251,6 +258,58 @@ module Exwiw
|
|
|
251
258
|
tables_from_models + build_rails_managed_tables(conn)
|
|
252
259
|
end
|
|
253
260
|
|
|
261
|
+
# The `columns` entries for a table: just the name, or — in safe mode — also
|
|
262
|
+
# a default mask and the `needs_mask_decision` flag. The primary key and the
|
|
263
|
+
# foreign keys/types the belongs_tos join on are flagged but never masked,
|
|
264
|
+
# since masking them would break the joins.
|
|
265
|
+
private def build_columns(representative, primary_key, belongs_tos, conn)
|
|
266
|
+
names = representative.column_names
|
|
267
|
+
return names.map { |name| { name: name } } unless @safe_new_columns
|
|
268
|
+
|
|
269
|
+
structural = belongs_tos.flat_map { |bt| [bt[:foreign_key], bt[:foreign_type]] }.compact.to_set
|
|
270
|
+
structural << primary_key if primary_key
|
|
271
|
+
columns_by_name = representative.columns.each_with_object({}) { |column, acc| acc[column.name] = column }
|
|
272
|
+
unique = unique_column_names(conn, representative.table_name)
|
|
273
|
+
defaults = representative.column_defaults
|
|
274
|
+
|
|
275
|
+
names.map do |name|
|
|
276
|
+
entry = { name: name, needs_mask_decision: true }
|
|
277
|
+
next entry if structural.include?(name)
|
|
278
|
+
|
|
279
|
+
column = columns_by_name[name]
|
|
280
|
+
mask = column && DefaultMask.for(
|
|
281
|
+
name: name,
|
|
282
|
+
type: column.type,
|
|
283
|
+
limit: column.limit,
|
|
284
|
+
primary_key: primary_key,
|
|
285
|
+
# `integer[]` reports `:integer`, so a scalar default would not fit.
|
|
286
|
+
array: column.respond_to?(:array?) && column.array?,
|
|
287
|
+
unique: unique.nil? || unique.include?(name),
|
|
288
|
+
column_default: defaults[name],
|
|
289
|
+
)
|
|
290
|
+
mask.nil? ? entry : entry.merge(replace_with: mask)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Columns covered by a unique index, or nil when they could not be read —
|
|
295
|
+
# callers treat that as "assume every column is unique", since masking a
|
|
296
|
+
# unique column with a constant breaks the restore the dump feeds.
|
|
297
|
+
#
|
|
298
|
+
# `Array()` because an expression index reports a String (`lower((email)::text)`)
|
|
299
|
+
# rather than a list; wrapping keeps it from being walked character by
|
|
300
|
+
# character, and it simply never matches a column name.
|
|
301
|
+
private def unique_column_names(conn, table_name)
|
|
302
|
+
conn.indexes(table_name).select(&:unique).flat_map { |index| Array(index.columns) }.to_set
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
# Once per run: a systematic failure would otherwise repeat per table.
|
|
305
|
+
unless @warned_missing_indexes
|
|
306
|
+
@warned_missing_indexes = true
|
|
307
|
+
warn "exwiw: could not read the indexes of '#{table_name}' (#{e.class}); " \
|
|
308
|
+
"treating every column as unique-indexed so no constant mask is emitted."
|
|
309
|
+
end
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
254
313
|
private def concrete_models
|
|
255
314
|
@models.reject(&:abstract_class?).select(&:table_exists?)
|
|
256
315
|
end
|
data/lib/exwiw/table_column.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Exwiw
|
|
|
5
5
|
include Serdes
|
|
6
6
|
|
|
7
7
|
attribute :name, String
|
|
8
|
-
attribute :replace_with,
|
|
8
|
+
attribute :replace_with, Serdes::OptionalType.new(MaskValue.new), skip_serializing_if_nil: true
|
|
9
9
|
attribute :raw_sql, optional(String), skip_serializing_if_nil: true
|
|
10
10
|
# Ruby-process-side masking modes, applied to the fetched rows by
|
|
11
11
|
# RowTransformer (SQL adapters only) — unlike replace_with/raw_sql, which
|
|
@@ -17,6 +17,12 @@ module Exwiw
|
|
|
17
17
|
# INSERT) once the config is loaded (see TableConfig#reject_ignored_members!).
|
|
18
18
|
attribute :comment, optional(String), skip_serializing_if_nil: true
|
|
19
19
|
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
20
|
+
# Marks a column whose masking nobody has decided on yet. Extraction never
|
|
21
|
+
# reads it; `schema:generate` emits it and `schema:check` reports it, so CI
|
|
22
|
+
# can require the decision.
|
|
23
|
+
attribute :needs_mask_decision,
|
|
24
|
+
Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)),
|
|
25
|
+
skip_serializing_if_nil: true
|
|
20
26
|
|
|
21
27
|
def self.from_symbol_keys(hash)
|
|
22
28
|
from(hash.transform_keys(&:to_s))
|
data/lib/exwiw/table_config.rb
CHANGED
|
@@ -170,9 +170,9 @@ module Exwiw
|
|
|
170
170
|
merged_table.type = passed_table.type
|
|
171
171
|
merged_table.comment = comment
|
|
172
172
|
merged_table.filter = filter
|
|
173
|
-
merged_table.bulk_insert_chunk_size = passed_table.bulk_insert_chunk_size
|
|
174
173
|
merged_table.ignore = ignore
|
|
175
174
|
# User-owned, never regenerated: carry over from the existing config.
|
|
175
|
+
merged_table.bulk_insert_chunk_size = bulk_insert_chunk_size
|
|
176
176
|
merged_table.scope_exempt = scope_exempt
|
|
177
177
|
merged_table.scope_column = scope_column
|
|
178
178
|
merged_table.reverse_scope = reverse_scope
|
|
@@ -259,7 +259,7 @@ module Exwiw
|
|
|
259
259
|
ruby_side = [column.map && "map", column.replace_with_fake_data && "replace_with_fake_data"].compact
|
|
260
260
|
return if ruby_side.empty?
|
|
261
261
|
|
|
262
|
-
sql_side = [column.raw_sql && "raw_sql", column.replace_with && "replace_with"].
|
|
262
|
+
sql_side = [column.raw_sql && "raw_sql", !column.replace_with.nil? && "replace_with"].select { |v| v }
|
|
263
263
|
if ruby_side.size > 1 || sql_side.any?
|
|
264
264
|
raise ArgumentError,
|
|
265
265
|
"Table '#{name}' column '#{column.name}': #{(ruby_side + sql_side).join('/')} cannot be combined; " \
|
data/lib/exwiw/version.rb
CHANGED
data/lib/exwiw.rb
CHANGED
|
@@ -10,6 +10,8 @@ require_relative "exwiw/config_file"
|
|
|
10
10
|
require_relative "exwiw/strict_keys"
|
|
11
11
|
require_relative "exwiw/belongs_to"
|
|
12
12
|
require_relative "exwiw/fake_data"
|
|
13
|
+
require_relative "exwiw/mask_value"
|
|
14
|
+
require_relative "exwiw/default_mask"
|
|
13
15
|
require_relative "exwiw/table_column"
|
|
14
16
|
require_relative "exwiw/reverse_scope"
|
|
15
17
|
require_relative "exwiw/batch_scope"
|
|
@@ -38,6 +40,11 @@ require_relative "exwiw/after_insert_hook"
|
|
|
38
40
|
require_relative "exwiw/runner"
|
|
39
41
|
require_relative "exwiw/explain_runner"
|
|
40
42
|
require_relative "exwiw/schema_generator"
|
|
43
|
+
require_relative "exwiw/db_introspector"
|
|
44
|
+
require_relative "exwiw/db_introspector/mysql_introspector"
|
|
45
|
+
require_relative "exwiw/db_introspector/postgresql_introspector"
|
|
46
|
+
require_relative "exwiw/db_schema_generator"
|
|
47
|
+
require_relative "exwiw/schema_check"
|
|
41
48
|
require_relative "exwiw/mongoid_schema_generator"
|
|
42
49
|
|
|
43
50
|
begin
|
data/lib/tasks/exwiw.rake
CHANGED
|
@@ -13,12 +13,18 @@ namespace :exwiw do
|
|
|
13
13
|
ENV["EXWIW_SCHEMA_DIR_PATH"] || Exwiw::ConfigFile.schema_dir || "exwiw/schema"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Safe mode is on unless EXWIW_NEW_COLUMNS=plain, which a first-time
|
|
17
|
+
# bootstrap wants: there every column is new, so safe mode would flag the
|
|
18
|
+
# whole config at once. See SchemaGenerator#initialize.
|
|
19
|
+
safe_new_columns = lambda { ENV["EXWIW_NEW_COLUMNS"] != "plain" }
|
|
20
|
+
|
|
16
21
|
desc "Generate schema from application"
|
|
17
22
|
task generate: :environment do
|
|
18
23
|
require "exwiw"
|
|
19
24
|
|
|
20
25
|
groups = Exwiw::SchemaGenerator.from_rails_application(
|
|
21
26
|
output_dir: resolve_schema_dir.call,
|
|
27
|
+
safe_new_columns: safe_new_columns.call,
|
|
22
28
|
).generate!
|
|
23
29
|
|
|
24
30
|
# Surface cross-database belongs_tos the generator auto-ignored: these
|
|
@@ -38,6 +44,24 @@ namespace :exwiw do
|
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
desc "Report how the committed schema config differs from the application, without changing it"
|
|
48
|
+
task check: :environment do
|
|
49
|
+
require "exwiw"
|
|
50
|
+
|
|
51
|
+
report = Exwiw::SchemaCheck.from_rails_application(schema_dir: resolve_schema_dir.call).run
|
|
52
|
+
json = JSON.pretty_generate(report)
|
|
53
|
+
puts json
|
|
54
|
+
# A file too, so a caller need not assume stdout carries only the JSON.
|
|
55
|
+
File.write(ENV["EXWIW_SCHEMA_CHECK_OUTPUT"], json + "\n") if ENV["EXWIW_SCHEMA_CHECK_OUTPUT"]
|
|
56
|
+
|
|
57
|
+
unless Exwiw::SchemaCheck.clean?(report)
|
|
58
|
+
$stderr.puts "exwiw: the schema config is out of date or has undecided masking; " \
|
|
59
|
+
"run `rake exwiw:schema:generate exwiw:schema:tidy` " \
|
|
60
|
+
"and resolve every `needs_mask_decision` column."
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
41
65
|
desc "Remove tables/columns from the schema config that no longer exist in the application"
|
|
42
66
|
task tidy: :environment do
|
|
43
67
|
require "exwiw"
|
|
@@ -76,7 +100,44 @@ namespace :exwiw do
|
|
|
76
100
|
Exwiw::MongoidSchemaGenerator.from_rails_application(
|
|
77
101
|
output_dir: resolve_schema_dir.call,
|
|
78
102
|
skip_unsupported: ENV["EXWIW_SKIP_UNSUPPORTED"] == "1",
|
|
103
|
+
# Same convention as `generate`: safe unless EXWIW_NEW_COLUMNS=plain.
|
|
104
|
+
safe_new_columns: safe_new_columns.call,
|
|
79
105
|
).generate!
|
|
80
106
|
end
|
|
107
|
+
|
|
108
|
+
desc "Remove collections from the schema config that no longer exist in the Mongoid application"
|
|
109
|
+
task tidy_mongoid: :environment do
|
|
110
|
+
require "exwiw"
|
|
111
|
+
|
|
112
|
+
result = Exwiw::MongoidSchemaGenerator.from_rails_application(
|
|
113
|
+
output_dir: resolve_schema_dir.call,
|
|
114
|
+
).tidy!
|
|
115
|
+
|
|
116
|
+
if result.empty?
|
|
117
|
+
puts "exwiw: schema config is already tidy; nothing to remove."
|
|
118
|
+
else
|
|
119
|
+
result.removed_tables.each do |name|
|
|
120
|
+
puts "exwiw: removed config for collection '#{name}' (no longer exists in the application)."
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
desc "Report how the committed schema config differs from the Mongoid application, without changing it"
|
|
126
|
+
task check_mongoid: :environment do
|
|
127
|
+
require "exwiw"
|
|
128
|
+
|
|
129
|
+
report = Exwiw::SchemaCheck.from_mongoid_application(schema_dir: resolve_schema_dir.call).run
|
|
130
|
+
json = JSON.pretty_generate(report)
|
|
131
|
+
puts json
|
|
132
|
+
# A file too, so a caller need not assume stdout carries only the JSON.
|
|
133
|
+
File.write(ENV["EXWIW_SCHEMA_CHECK_OUTPUT"], json + "\n") if ENV["EXWIW_SCHEMA_CHECK_OUTPUT"]
|
|
134
|
+
|
|
135
|
+
unless Exwiw::SchemaCheck.clean?(report)
|
|
136
|
+
$stderr.puts "exwiw: the schema config is out of date or has undecided masking; " \
|
|
137
|
+
"run `rake exwiw:schema:generate_mongoid exwiw:schema:tidy_mongoid` " \
|
|
138
|
+
"and resolve every `needs_mask_decision` field."
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
81
142
|
end
|
|
82
143
|
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.9.
|
|
4
|
+
version: 0.9.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shia
|
|
@@ -69,13 +69,19 @@ files:
|
|
|
69
69
|
- lib/exwiw/belongs_to.rb
|
|
70
70
|
- lib/exwiw/cli.rb
|
|
71
71
|
- lib/exwiw/config_file.rb
|
|
72
|
+
- lib/exwiw/db_introspector.rb
|
|
73
|
+
- lib/exwiw/db_introspector/mysql_introspector.rb
|
|
74
|
+
- lib/exwiw/db_introspector/postgresql_introspector.rb
|
|
75
|
+
- lib/exwiw/db_schema_generator.rb
|
|
72
76
|
- lib/exwiw/ddl_postprocessor.rb
|
|
77
|
+
- lib/exwiw/default_mask.rb
|
|
73
78
|
- lib/exwiw/determine_table_processing_order.rb
|
|
74
79
|
- lib/exwiw/embedded_in.rb
|
|
75
80
|
- lib/exwiw/explain_runner.rb
|
|
76
81
|
- lib/exwiw/ext_json.rb
|
|
77
82
|
- lib/exwiw/fake_data.rb
|
|
78
83
|
- lib/exwiw/japanese_names.rb
|
|
84
|
+
- lib/exwiw/mask_value.rb
|
|
79
85
|
- lib/exwiw/mongo_query.rb
|
|
80
86
|
- lib/exwiw/mongodb_collection_config.rb
|
|
81
87
|
- lib/exwiw/mongodb_field.rb
|
|
@@ -88,6 +94,7 @@ files:
|
|
|
88
94
|
- lib/exwiw/reverse_scope.rb
|
|
89
95
|
- lib/exwiw/row_transformer.rb
|
|
90
96
|
- lib/exwiw/runner.rb
|
|
97
|
+
- lib/exwiw/schema_check.rb
|
|
91
98
|
- lib/exwiw/schema_generator.rb
|
|
92
99
|
- lib/exwiw/strict_keys.rb
|
|
93
100
|
- lib/exwiw/table_column.rb
|