Class: Exwiw::SchemaGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/exwiw/schema_generator.rb

Defined Under Namespace

Classes: TidyResult

Constant Summary collapse

ACTIVE_STORAGE_VARIANT_RECORDS_TABLE =

ActiveStorage tracks generated image variants in this table. Its rows are derivative and regenerable — ActiveStorage lazily (re)creates a variant the next time it is requested — so there is little value in exporting them. More importantly, the table has no belongs_to path to any dump target, which would land it in QueryAstBuilder's "no relation -> dump all" branch, while its blob_id references active_storage_blobs, which the reverse "referenced_by" extraction narrows to only the attachment-referenced blobs. A full variant_records dump can therefore reference blobs that were never exported (a foreign-key violation on import). So the table is emitted with ignore: true (data extraction skipped) and excluded as a polymorphic record target so the non-ignored attachments table carries no dangling belongs_to to it.

"active_storage_variant_records"
CROSS_DATABASE_IGNORE_TYPE =
"cross_database"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(models:, output_dir:, safe_new_columns: true) ⇒ SchemaGenerator

safe_new_columns (the default) emits every column masked — as far as its type allows, see DefaultMask — and flagged needs_mask_decision: true. #merge lets an existing entry win, so in practice only columns a migration has just added keep that treatment. Pass false to bootstrap a config, where every column is new and flagging all of them at once is noise.



56
57
58
59
60
# File 'lib/exwiw/schema_generator.rb', line 56

def initialize(models:, output_dir:, safe_new_columns: true)
  @models = models
  @output_dir = output_dir
  @safe_new_columns = safe_new_columns
end

Class Method Details

.cross_database_belongs_tos(groups) ⇒ Object

Flatten the generated groups (the Hash returned by generate! / build_table_groups) into the list of cross-database belongs_tos the generator auto-ignored, so a caller (the rake task) can surface them. Each entry is { table:, foreign_key:, target: }. Empty for single-database apps.



72
73
74
75
76
77
78
79
80
# File 'lib/exwiw/schema_generator.rb', line 72

def self.cross_database_belongs_tos(groups)
  groups.values.flatten.flat_map do |table|
    next [] unless table.respond_to?(:belongs_tos)

    table.belongs_tos
         .select { |bt| bt.ignore_type == CROSS_DATABASE_IGNORE_TYPE }
         .map { |bt| { table: table.name, foreign_key: bt.foreign_key, target: bt.table_name } }
  end
end

.from_rails_application(output_dir:, safe_new_columns: true) ⇒ Object



46
47
48
49
# File 'lib/exwiw/schema_generator.rb', line 46

def self.from_rails_application(output_dir:, safe_new_columns: true)
  Rails.application.eager_load!
  new(models: ActiveRecord::Base.descendants, output_dir: output_dir, safe_new_columns: safe_new_columns)
end

Instance Method Details

#build_table_groupsObject

Returns a Hash keyed by the database name.

  • Single-database setup: the only key is nil, signalling that the table configs should be written flat into output_dir (backwards compatible).
  • Multi-database setup (Rails connects_to): one key per database (connection_db_config.name, e.g. "primary" / "analytics"), each mapping to that database's table configs. They are written into output_dir/<db_name>/.


144
145
146
147
148
# File 'lib/exwiw/schema_generator.rb', line 144

def build_table_groups
  model_db_groups.each_with_object({}) do |(db_name, group_models, conn), result|
    result[db_name] = build_tables_for(group_models, conn)
  end
end

#build_tablesObject

Backwards-compatible flat list of all table configs. Only meaningful for a single-database setup; for multi-database setups prefer #build_table_groups so the database association is preserved.



179
180
181
# File 'lib/exwiw/schema_generator.rb', line 179

def build_tables
  build_table_groups.values.flatten
end

#generate!Object



62
63
64
65
66
# File 'lib/exwiw/schema_generator.rb', line 62

def generate!
  groups = build_table_groups
  write_groups(groups)
  groups
end

#tidy!Object

Reconcile the config files already on disk against the live database, removing only what no longer exists there:

  • a config file whose table is no longer present is deleted, and
  • columns recorded in a surviving table's config that the table no longer has are dropped from that file.

The source of truth is the database connection (data_sources for table existence — which covers views too — and columns for the column list), NOT build_table_groups. build_table_groups only knows about tables that still have an ActiveRecord model, so reconciling against it would delete the config of a table that is still present in the database but has merely lost (or never had) a model. Reading the connection directly avoids that: only a table that is genuinely gone from the database is removed.

Unlike generate!, tidy never adds or regenerates entries: every surviving table/column — including its hand-edited comment / ignore / replace_with — is left untouched, and only the stale entries are stripped. (Removing a deleted column is something generate! already does incidentally via #merge, but generate! can never delete the config file of a removed table, which is the gap this fills.) Returns a TidyResult describing the removals so callers (e.g. the rake task) can report them.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/exwiw/schema_generator.rb', line 105

def tidy!
  result = TidyResult.new

  model_db_groups.each do |db_name, _group_models, conn|
    dir = config_dir_for(db_name)
    next unless Dir.exist?(dir)

    existing_data_sources = conn.data_sources.to_set

    Dir[File.join(dir, "*.json")].sort.each do |path|
      existing = TableConfig.from(JSON.parse(File.read(path)))

      unless existing_data_sources.include?(existing.name)
        File.delete(path)
        result.add_removed_table(existing.name)
        next
      end

      valid_column_names = conn.columns(existing.name).map(&:name).to_set
      stale_columns = existing.columns.reject { |column| valid_column_names.include?(column.name) }
      next if stale_columns.empty?

      existing.columns = existing.columns.select { |column| valid_column_names.include?(column.name) }
      File.write(path, JSON.pretty_generate(existing.to_hash) + "\n")
      stale_columns.each { |column| result.add_removed_column(existing.name, column.name) }
    end
  end

  result
end

#write_files(dir, tables) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/exwiw/schema_generator.rb', line 197

def write_files(dir, tables)
  FileUtils.mkdir_p(dir)

  tables.each do |table|
    path = File.join(dir, "#{table.name}.json")
    config_to_write =
      if File.exist?(path)
        TableConfig.from(JSON.parse(File.read(path))).merge(table)
      else
        table
      end
    File.write(path, JSON.pretty_generate(config_to_write.to_hash) + "\n")
  end
end

#write_groups(groups) ⇒ Object



183
184
185
186
187
# File 'lib/exwiw/schema_generator.rb', line 183

def write_groups(groups)
  groups.each do |db_name, tables|
    write_files(config_dir_for(db_name), tables)
  end
end