exwiw 0.1.5 → 0.1.6

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: 2f9fb9e57ed24338ac642aa9f0aa200980d8b57fd2d73851249467f9ec9e251b
4
- data.tar.gz: 2cb68bd84f67b9a683116d83daacab6a96b65e4802f8b67afb81ef97ebccf3c7
3
+ metadata.gz: c3fe2fb0c2cff1c962899799f4cfe22981a7b730759d456dc01ab8d19254dbec
4
+ data.tar.gz: 7bf651780e74074b50e9b0345a7cdcb9ec0281cd3d60ff04764cf03c1804b32e
5
5
  SHA512:
6
- metadata.gz: 59bc123c523bf1f97dd97b6c18aa93972926366f121dda9dad9105cf651a783a77b38d7547fad99b7ab1c9509452123bf1c8423203cb0757cf3161d64d296c84
7
- data.tar.gz: 6ea7278185d5f92bea6b43d3fb164bb23ae6557ce5d1f3d27e2a86a5147e665a7228ae848e2340c41fd1c5d43ccbdb81decf71804c4d68283de3c61b17be701c
6
+ metadata.gz: a8ee7520812cb8a655cd4f9b8dffa3afdd622311c6b39c5b7942aae5743c3642cb44a7b6b2fdd56ffdf967a8225c220866691a8a001bf75606c70871d4d731ca
7
+ data.tar.gz: 10182f0c69f0d773e1a515f932148c7b8795a51ba64e46141d0630459c815594e470ac62e4d082a695426ec254430dcac6c3cdbaff74b47e34c49f0dfa9a901a
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ plugins:
2
+ - rubocop-greppable_rails
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.3
6
+ DisabledByDefault: true
7
+ SuggestExtensions: false
8
+
9
+ GreppableRails/UseInlineAccessModifier:
10
+ Enabled: true
data/CHANGELOG.md CHANGED
@@ -5,16 +5,24 @@
5
5
  ### Added
6
6
 
7
7
  - Add `bulk_insert_chunk_size` table config to split the generated `INSERT` statement into chunks of the specified size. ([#8](https://github.com/riseshia/exwiw/pull/8))
8
+ - Add experimental MongoDB adapter (`--adapter=mongodb`) that exports collections as JSONL (`insert-{idx}-{collection}.jsonl`). Embedded documents and collection-level `filter` are not supported. ([#10](https://github.com/riseshia/exwiw/pull/10))
9
+ - Introduce `MongodbCollectionConfig` for the MongoDB adapter, with MongoDB-native naming (`fields` instead of `columns`). ([#10](https://github.com/riseshia/exwiw/pull/10))
8
10
 
9
11
  ### Changed
10
12
 
13
+ - **Breaking (MongoDB only):** scenario JSON for the MongoDB adapter must use `fields` instead of `columns`. SQL adapters (`mysql2`, `postgresql`, `sqlite3`) are unaffected. ([#10](https://github.com/riseshia/exwiw/pull/10))
11
14
  - Bump minimum required Ruby version to 3.3.0 and drop Ruby 3.2 from the CI matrix (3.2 reached EOL on 2026-03-31).
15
+ - Refactor `Adapter` contract to support non-SQL backends: introduce `Adapter#build_query`, `Adapter#output_extension`, and `Adapter#supports_bulk_delete?` hooks. SQL adapters retain existing behavior. ([#9](https://github.com/riseshia/exwiw/pull/9))
16
+ - Extract `exwiw:schema:generate` logic into `Exwiw::SchemaGenerator` so it can be exercised under RSpec without the Rake harness. ([#11](https://github.com/riseshia/exwiw/pull/11))
12
17
 
13
18
  ### Fixed
14
19
 
15
20
  - Fix MySQL host access for local rspec runs and switch local dev scripts to inject the password via `MYSQL_PWD` env on `docker compose exec` instead of the `-p` CLI flag. ([#5](https://github.com/riseshia/exwiw/pull/5))
16
21
  - Expand `~` in path arguments and validate the existence of `--config-dir`. ([#6](https://github.com/riseshia/exwiw/pull/6))
17
22
  - Fix incorrect left-side table in `JOIN ... ON` clause for join chains with 3+ hops, which caused `no such column` / `column does not exist` errors at execute time. ([#7](https://github.com/riseshia/exwiw/pull/7))
23
+ - Fix hard-coded `id` primary key in `QueryAstBuilder` so non-`id` primary keys are honored when expanding `dump_target.ids` into `WHERE`. ([#9](https://github.com/riseshia/exwiw/pull/9))
24
+ - `exwiw:schema:generate` now aggregates `belongs_to` reflections across STI subclasses sharing one table; previously the first-seen class won and subclass associations could be silently dropped. ([#11](https://github.com/riseshia/exwiw/pull/11))
25
+ - `exwiw:schema:generate` now fails fast with `Exwiw::SchemaGenerator::MultipleDatabasesNotSupportedError` when models span multiple `connects_to` databases instead of silently producing a partial schema bound to a single connection. ([#11](https://github.com/riseshia/exwiw/pull/11))
18
26
 
19
27
  ## [0.1.4] - 2026-04-04
20
28
 
data/README.md CHANGED
@@ -40,6 +40,7 @@ gem install exwiw
40
40
  - mysql2
41
41
  - postgresql
42
42
  - sqlite3
43
+ - mongodb (experimental, see [MongoDB notes](#mongodb-notes))
43
44
 
44
45
  ## Usage
45
46
 
@@ -170,6 +171,21 @@ Notice this is the most powerful option, but you should be careful to use this o
170
171
  Because this transformation occured on exwiw process, so much slower than other options.
171
172
  Most of case, this option is not recommended.
172
173
 
174
+ ### MongoDB notes
175
+
176
+ The MongoDB adapter is experimental. To use it:
177
+
178
+ - Add `gem "mongo"` to your Gemfile in addition to `exwiw` (it is not declared as a runtime dependency of the gem).
179
+ - Set `--adapter=mongodb`. `--user` / `DATABASE_PASSWORD` are optional and only needed when your MongoDB requires authentication.
180
+ - The MongoDB adapter consumes a separate config type, `MongodbCollectionConfig`, with MongoDB-native naming. Use `fields` (instead of the SQL adapters' `columns`), and set `"primary_key": "_id"`. Foreign keys (`shop_id`, `user_id`, ...) stay as ordinary fields.
181
+ - Output is JSON Lines (`insert-{idx}-{collection}.jsonl`) using MongoDB Extended JSON (relaxed mode). Import with `mongoimport`:
182
+ ```bash
183
+ mongoimport --db app_dev --collection users --file dump/insert-002-users.jsonl
184
+ ```
185
+ - Unlike SQL adapters, the MongoDB adapter does not emit `delete-*.jsonl` files (drop the database / collection yourself before importing if needed).
186
+ - `raw_sql` is not supported (the `MongodbField` schema does not declare it; any `raw_sql` keys in scenario JSON are silently dropped on load). Use `replace_with` for masking.
187
+ - The MongoDB adapter does not support the collection-level `filter` field (it raises `NotImplementedError` if set, since the SQL-string filter cannot be applied to MongoDB).
188
+
173
189
  ## How it works
174
190
 
175
191
  - Load the table information from the specified config file.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # NOTE: This adapter consumes MongodbCollectionConfig (`fields` instead of
6
+ # the SQL adapters' `columns`). It assumes a "flat" document schema where
7
+ # references between collections are expressed as scalar foreign keys
8
+ # (e.g. `shop_id` on `users`); the forward fan-out strategy here cannot
9
+ # follow references that live inside embedded structures.
10
+ module Exwiw
11
+ module Adapter
12
+ class MongodbAdapter < Base
13
+ def self.table_config_class
14
+ Exwiw::MongodbCollectionConfig
15
+ end
16
+
17
+ def initialize(connection_config, logger)
18
+ super
19
+ @state = {}
20
+ end
21
+
22
+ def build_query(config, dump_target, _config_by_name)
23
+ reject_filter!(config)
24
+
25
+ filter =
26
+ if config.name == dump_target.table_name
27
+ { config.primary_key => { "$in" => coerce_ids(dump_target.ids) } }
28
+ else
29
+ constrained = config.belongs_tos.select do |relation|
30
+ @state.key?(relation.table_name) && !@state[relation.table_name].empty?
31
+ end
32
+
33
+ if constrained.empty?
34
+ {}
35
+ else
36
+ constrained.each_with_object({}) do |relation, acc|
37
+ acc[relation.foreign_key] = { "$in" => @state[relation.table_name] }
38
+ end
39
+ end
40
+ end
41
+
42
+ Exwiw::MongoQuery::Find.new(
43
+ collection: config.name,
44
+ primary_key: config.primary_key,
45
+ filter: filter,
46
+ projection: build_projection(config),
47
+ )
48
+ end
49
+
50
+ def execute(query)
51
+ @logger.debug(" Executing Mongo find on '#{query.collection}': filter=#{query.filter.inspect} projection=#{query.projection.inspect}")
52
+
53
+ docs = db[query.collection].find(query.filter).projection(query.projection).to_a
54
+
55
+ @state[query.collection] = docs.map { |doc| doc[query.primary_key] }
56
+
57
+ docs
58
+ end
59
+
60
+ def to_bulk_insert(rows, config)
61
+ rows.map do |doc|
62
+ apply_replace_with!(doc, config)
63
+ JSON.generate(extended_json(doc))
64
+ end.join("\n")
65
+ end
66
+
67
+ def to_bulk_delete(_query, _config)
68
+ raise NotImplementedError, "MongodbAdapter does not support bulk delete"
69
+ end
70
+
71
+ def output_extension
72
+ 'jsonl'
73
+ end
74
+
75
+ def supports_bulk_delete?
76
+ false
77
+ end
78
+
79
+ # `--ids` from the CLI arrives as Strings. Mongo compares types strictly,
80
+ # so integer-looking ids are coerced to Integer. Other strings (e.g. ObjectId
81
+ # hex) are left as-is.
82
+ private def coerce_ids(ids)
83
+ Array(ids).map do |id|
84
+ if id.is_a?(String) && id.match?(/\A-?\d+\z/)
85
+ id.to_i
86
+ else
87
+ id
88
+ end
89
+ end
90
+ end
91
+
92
+ private def reject_filter!(config)
93
+ return if config.filter.nil? || config.filter.to_s.empty?
94
+
95
+ raise NotImplementedError,
96
+ "collection-level `filter` is not supported by MongodbAdapter (collection: #{config.name})"
97
+ end
98
+
99
+ private def build_projection(config)
100
+ projection = {}
101
+ # Always include primary key so masking templates referencing it work,
102
+ # even if it is not declared in fields.
103
+ projection[config.primary_key] = 1
104
+ config.fields.each do |field|
105
+ projection[field.name] = 1
106
+ end
107
+ projection
108
+ end
109
+
110
+ private def apply_replace_with!(doc, config)
111
+ config.fields.each do |field|
112
+ next unless field.replace_with
113
+
114
+ doc[field.name] = field.replace_with.gsub(/\{([^{}]+)\}/) do
115
+ ref = Regexp.last_match(1)
116
+ (doc.key?(ref) ? doc[ref] : nil).to_s
117
+ end
118
+ end
119
+ end
120
+
121
+ private def extended_json(doc)
122
+ if doc.respond_to?(:as_extended_json)
123
+ doc.as_extended_json(mode: :relaxed)
124
+ else
125
+ doc
126
+ end
127
+ end
128
+
129
+ private def db
130
+ @db ||=
131
+ begin
132
+ require 'mongo'
133
+ address = "#{@connection_config.host}:#{@connection_config.port}"
134
+ options = { database: @connection_config.database_name }
135
+ if @connection_config.user && !@connection_config.user.to_s.empty?
136
+ options[:user] = @connection_config.user
137
+ options[:password] = @connection_config.password
138
+ end
139
+ Mongo::Logger.logger.level = ::Logger::WARN
140
+ Mongo::Client.new([address], **options)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -3,6 +3,10 @@
3
3
  module Exwiw
4
4
  module Adapter
5
5
  class Mysql2Adapter < Base
6
+ def build_query(table, dump_target, table_by_name)
7
+ Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
8
+ end
9
+
6
10
  def execute(query_ast)
7
11
  sql = compile_ast(query_ast)
8
12
 
@@ -3,6 +3,10 @@
3
3
  module Exwiw
4
4
  module Adapter
5
5
  class PostgresqlAdapter < Base
6
+ def build_query(table, dump_target, table_by_name)
7
+ Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
8
+ end
9
+
6
10
  def execute(query_ast)
7
11
  sql = compile_ast(query_ast)
8
12
 
@@ -3,6 +3,10 @@
3
3
  module Exwiw
4
4
  module Adapter
5
5
  class Sqlite3Adapter < Base
6
+ def build_query(table, dump_target, table_by_name)
7
+ Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
8
+ end
9
+
6
10
  def execute(query_ast)
7
11
  sql = compile_ast(query_ast)
8
12
 
data/lib/exwiw/adapter.rb CHANGED
@@ -9,6 +9,31 @@ module Exwiw
9
9
  @connection_config = connection_config
10
10
  @logger = logger
11
11
  end
12
+
13
+ # The config class that this adapter consumes. Runner uses this to
14
+ # decide which Serdes type to load scenario JSON into. SQL adapters
15
+ # share the SQL-shaped TableConfig; non-SQL adapters override.
16
+ def self.table_config_class
17
+ TableConfig
18
+ end
19
+
20
+ # @params [Exwiw::TableConfig] table
21
+ # @params [Exwiw::DumpTarget] dump_target
22
+ # @params [Hash{String => Exwiw::TableConfig}] table_by_name
23
+ # @return [Object] adapter-specific query object (e.g. Exwiw::QueryAst::Select for SQL)
24
+ def build_query(table, dump_target, table_by_name)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ # File extension used for dump output (e.g. 'sql' for SQL, 'jsonl' for MongoDB).
29
+ def output_extension
30
+ 'sql'
31
+ end
32
+
33
+ # Whether this adapter emits delete-NNN-*.sql files.
34
+ def supports_bulk_delete?
35
+ true
36
+ end
12
37
  end
13
38
 
14
39
  # @params [Exwiw::QueryAst] query_ast
@@ -36,6 +61,8 @@ module Exwiw
36
61
  Adapter::Mysql2Adapter.new(connection_config, logger)
37
62
  when 'postgresql'
38
63
  Adapter::PostgresqlAdapter.new(connection_config, logger)
64
+ when 'mongodb'
65
+ Adapter::MongodbAdapter.new(connection_config, logger)
39
66
  else
40
67
  raise 'Unsupported adapter'
41
68
  end
data/lib/exwiw/cli.rb CHANGED
@@ -67,25 +67,26 @@ module Exwiw
67
67
 
68
68
  private def validate_options!
69
69
  if @database_adapter != "sqlite3"
70
- {
70
+ required_options = {
71
71
  "Target database host" => @database_host,
72
72
  "Target database port" => @database_port,
73
- "Database user" => @database_user,
74
73
  "Target database name" => @database_name,
75
- }.each do |k, v|
74
+ }
75
+ required_options["Database user"] = @database_user unless @database_adapter == "mongodb"
76
+ required_options.each do |k, v|
76
77
  if v.nil?
77
78
  $stderr.puts "#{k} is required"
78
79
  exit 1
79
80
  end
80
81
  end
81
82
 
82
- if @database_password.nil? || @database_password.empty?
83
+ if @database_adapter != "mongodb" && (@database_password.nil? || @database_password.empty?)
83
84
  $stderr.puts "environment variable 'DATABASE_PASSWORD' is required"
84
85
  exit 1
85
86
  end
86
87
  end
87
88
 
88
- valid_adapters = ["mysql2", "postgresql", "sqlite3"]
89
+ valid_adapters = ["mysql2", "postgresql", "sqlite3", "mongodb"]
89
90
  unless valid_adapters.include?(@database_adapter)
90
91
  $stderr.puts "Invalid adapter. Available options are: #{valid_adapters.join(', ')}"
91
92
  exit 1
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exwiw
4
+ module MongoQuery
5
+ Find = Struct.new(:collection, :primary_key, :filter, :projection, keyword_init: true) do
6
+ def to_h
7
+ {
8
+ collection: collection,
9
+ primary_key: primary_key,
10
+ filter: filter,
11
+ projection: projection,
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exwiw
4
+ class MongodbCollectionConfig
5
+ include Serdes
6
+
7
+ # MongoDB-native names. Intentionally re-declared instead of inheriting
8
+ # from TableConfig — Serdes does not propagate attribute declarations
9
+ # across class boundaries.
10
+ attribute :name, String
11
+ attribute :primary_key, String
12
+ attribute :filter, optional(String), skip_serializing_if_nil: true
13
+ attribute :belongs_tos, array(BelongsTo)
14
+ attribute :fields, array(MongodbField)
15
+ attribute :bulk_insert_chunk_size, optional(Integer), skip_serializing_if_nil: true
16
+
17
+ def self.from_symbol_keys(hash)
18
+ from(JSON.parse(hash.to_json))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exwiw
4
+ class MongodbField
5
+ include Serdes
6
+
7
+ attribute :name, String
8
+ attribute :replace_with, optional(String), skip_serializing_if_nil: true
9
+
10
+ def self.from_symbol_keys(hash)
11
+ from(hash.transform_keys(&:to_s))
12
+ end
13
+
14
+ def to_hash
15
+ super.compact
16
+ end
17
+ end
18
+ end
@@ -32,7 +32,7 @@ module Exwiw
32
32
  {
33
33
  column_name: column_name,
34
34
  operator: operator,
35
- value: value
35
+ value: value,
36
36
  }
37
37
  end
38
38
  end
@@ -77,7 +77,7 @@ module Exwiw
77
77
 
78
78
  if table.name == dump_target.table_name
79
79
  clauses.push Exwiw::QueryAst::WhereClause.new(
80
- column_name: 'id',
80
+ column_name: table.primary_key,
81
81
  operator: :eq,
82
82
  value: dump_target.ids
83
83
  )
data/lib/exwiw/runner.rb CHANGED
@@ -20,7 +20,7 @@ module Exwiw
20
20
 
21
21
  def run
22
22
  adapter = Adapter.build(@connection_config, @logger)
23
- tables = load_table_config
23
+ tables = load_table_config(adapter.class.table_config_class)
24
24
 
25
25
  @logger.info("Determining table processing order...")
26
26
  ordered_table_names = DetermineTableProcessingOrder.run(tables)
@@ -36,7 +36,7 @@ module Exwiw
36
36
  @logger.info("Processing table '#{table_name}'... (#{idx + 1}/#{total_size})")
37
37
  table = table_by_name.fetch(table_name)
38
38
 
39
- query_ast = QueryAstBuilder.run(table.name, table_by_name, @dump_target, @logger)
39
+ query_ast = adapter.build_query(table, @dump_target, table_by_name)
40
40
  results = adapter.execute(query_ast)
41
41
  record_num = results.size
42
42
 
@@ -44,36 +44,38 @@ module Exwiw
44
44
  @logger.info(" No records matched. skip this table.")
45
45
  next
46
46
  end
47
- @logger.debug(" Generate INSERT SQL...")
47
+ @logger.debug(" Generate INSERT statement...")
48
48
 
49
49
  chunk_size = table.bulk_insert_chunk_size
50
50
  chunks = chunk_size ? results.each_slice(chunk_size).to_a : [results]
51
51
  insert_sql = chunks.map { |chunk_rows| adapter.to_bulk_insert(chunk_rows, table) }.join("\n")
52
52
 
53
- @logger.info(" Generated INSERT SQL for #{record_num} records (#{chunks.size} statement(s)).")
53
+ @logger.info(" Generated INSERT statement for #{record_num} records (#{chunks.size} statement(s)).")
54
54
  insert_idx = (idx + 1).to_s.rjust(3, '0')
55
- File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.sql"), 'w') do |file|
55
+ File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
56
56
  file.puts(insert_sql)
57
57
  end
58
58
 
59
- @logger.debug(" Generate DELETE SQL...")
60
- delete_sql = adapter.to_bulk_delete(query_ast, table)
61
- if @logger.debug?
62
- @logger.debug(" Generated DELETE SQL:\n#{delete_sql}")
63
- else
64
- @logger.info(" Generated DELETE SQL.")
65
- end
66
- delete_idx = (total_size - idx).to_s.rjust(3, '0')
67
- File.open(File.join(@output_dir, "delete-#{delete_idx}-#{table_name}.sql"), 'w') do |file|
68
- file.puts(delete_sql)
59
+ if adapter.supports_bulk_delete?
60
+ @logger.debug(" Generate DELETE statement...")
61
+ delete_sql = adapter.to_bulk_delete(query_ast, table)
62
+ if @logger.debug?
63
+ @logger.debug(" Generated DELETE statement:\n#{delete_sql}")
64
+ else
65
+ @logger.info(" Generated DELETE statement.")
66
+ end
67
+ delete_idx = (total_size - idx).to_s.rjust(3, '0')
68
+ File.open(File.join(@output_dir, "delete-#{delete_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
69
+ file.puts(delete_sql)
70
+ end
69
71
  end
70
72
  end
71
73
  end
72
74
 
73
- private def load_table_config
75
+ private def load_table_config(klass)
74
76
  Dir[File.join(@config_dir, "*.json")].map do |file|
75
77
  json = JSON.parse(File.read(file))
76
- TableConfig.from(json)
78
+ klass.from(json)
77
79
  end
78
80
  end
79
81
 
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module Exwiw
7
+ class SchemaGenerator
8
+ class MultipleDatabasesNotSupportedError < StandardError; end
9
+
10
+ def self.from_rails_application(output_dir:)
11
+ Rails.application.eager_load!
12
+ new(models: ActiveRecord::Base.descendants, output_dir: output_dir)
13
+ end
14
+
15
+ def initialize(models:, output_dir:)
16
+ @models = models
17
+ @output_dir = output_dir
18
+ end
19
+
20
+ def generate!
21
+ tables = build_tables
22
+ write_files(tables)
23
+ tables
24
+ end
25
+
26
+ def build_tables
27
+ models = concrete_models
28
+ validate_single_database!(models)
29
+
30
+ models.group_by(&:table_name).map do |table_name, model_group|
31
+ representative = model_group.first
32
+ TableConfig.from_symbol_keys(
33
+ name: table_name,
34
+ primary_key: representative.primary_key,
35
+ belongs_tos: aggregate_belongs_tos(model_group),
36
+ columns: representative.column_names.map { |name| { name: name } },
37
+ )
38
+ end
39
+ end
40
+
41
+ def write_files(tables)
42
+ FileUtils.mkdir_p(@output_dir)
43
+
44
+ tables.each do |table|
45
+ path = File.join(@output_dir, "#{table.name}.json")
46
+ config_to_write =
47
+ if File.exist?(path)
48
+ TableConfig.from(JSON.parse(File.read(path))).merge(table)
49
+ else
50
+ table
51
+ end
52
+ File.write(path, JSON.pretty_generate(config_to_write.to_hash) + "\n")
53
+ end
54
+ end
55
+
56
+ private def concrete_models
57
+ @models.reject(&:abstract_class?).select(&:table_exists?)
58
+ end
59
+
60
+ private def aggregate_belongs_tos(models)
61
+ pairs = models
62
+ .flat_map { |m| m.reflect_on_all_associations(:belongs_to) }
63
+ .reject(&:polymorphic?) # XXX: Support polymorphic
64
+ .map { |assoc| [assoc.table_name, assoc.foreign_key] }
65
+ .uniq
66
+
67
+ pairs.map do |table_name, foreign_key|
68
+ { table_name: table_name, foreign_key: foreign_key }
69
+ end
70
+ end
71
+
72
+ # `connection_specification_name` is a quasi-private API but has been stable
73
+ # across Rails 6.1 - 8.x. With Rails multi-DB (`connects_to`), every
74
+ # descendant of the same abstract base shares one spec name regardless of
75
+ # role/shard, so distinct values across concrete models indicate genuinely
76
+ # separate databases.
77
+ private def validate_single_database!(models)
78
+ return if models.empty?
79
+
80
+ specs = models.map(&:connection_specification_name).uniq
81
+ return if specs.size <= 1
82
+
83
+ raise MultipleDatabasesNotSupportedError, <<~MSG
84
+ exwiw does not yet support Rails multiple-database setup.
85
+ Detected connection specifications: #{specs.inspect}
86
+ Track progress at https://github.com/riseshia/exwiw/issues
87
+ MSG
88
+ end
89
+ end
90
+ end
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.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/exwiw.rb CHANGED
@@ -5,17 +5,22 @@ require_relative "exwiw/version"
5
5
  require "json"
6
6
  require "serdes"
7
7
 
8
+ require_relative "exwiw/belongs_to"
9
+ require_relative "exwiw/table_column"
10
+ require_relative "exwiw/table_config"
11
+ require_relative "exwiw/mongodb_field"
12
+ require_relative "exwiw/mongodb_collection_config"
8
13
  require_relative "exwiw/adapter"
9
14
  require_relative "exwiw/adapter/sqlite3_adapter"
10
15
  require_relative "exwiw/adapter/mysql2_adapter"
11
16
  require_relative "exwiw/adapter/postgresql_adapter"
17
+ require_relative "exwiw/adapter/mongodb_adapter"
12
18
  require_relative "exwiw/determine_table_processing_order"
19
+ require_relative "exwiw/mongo_query"
13
20
  require_relative "exwiw/query_ast"
14
21
  require_relative "exwiw/query_ast_builder"
15
22
  require_relative "exwiw/runner"
16
- require_relative "exwiw/belongs_to"
17
- require_relative "exwiw/table_column"
18
- require_relative "exwiw/table_config"
23
+ require_relative "exwiw/schema_generator"
19
24
 
20
25
  begin
21
26
  require 'rails'
data/lib/tasks/exwiw.rake CHANGED
@@ -4,59 +4,11 @@ namespace :exwiw do
4
4
  namespace :schema do
5
5
  desc "Generate schema from application"
6
6
  task generate: :environment do
7
- require "json"
8
7
  require "exwiw"
9
- require "fileutils"
10
8
 
11
- Rails.application.eager_load!
12
-
13
- table_by_name = {}
14
-
15
- ActiveRecord::Base.descendants.each do |model|
16
- next if model.abstract_class?
17
- next unless model.table_exists?
18
- next if table_by_name[model.table_name]
19
-
20
- belongs_tos = model.reflect_on_all_associations(:belongs_to).map do |assoc|
21
- if assoc.polymorphic?
22
- # XXX: Support polymorphic
23
- next
24
- else
25
- Exwiw::BelongsTo.from_symbol_keys({
26
- table_name: assoc.table_name,
27
- foreign_key: assoc.foreign_key,
28
- })
29
- end
30
- end
31
-
32
- columns = model.column_names.map do |name|
33
- Exwiw::TableColumn.from_symbol_keys({ name: name })
34
- end
35
-
36
- table = Exwiw::TableConfig.from_symbol_keys({
37
- name: model.table_name,
38
- primary_key: model.primary_key,
39
- belongs_tos: belongs_tos.compact,
40
- columns: columns,
41
- })
42
- table_by_name[table.name] = table
43
- end
44
-
45
- tables = table_by_name.values
46
-
47
- output_dir = ENV['OUTPUT_DIR_PATH'] || "exwiw"
48
- FileUtils.mkdir_p(output_dir)
49
-
50
- tables.each do |table|
51
- path = File.join(output_dir, "#{table.name}.json")
52
- if File.exist?(path)
53
- current_config = Exwiw::TableConfig.from(JSON.parse(File.read(path)))
54
- merged_config = current_config.merge(table)
55
- File.write(path, JSON.pretty_generate(merged_config.to_hash) + "\n")
56
- else
57
- File.write(path, JSON.pretty_generate(table.to_hash) + "\n")
58
- end
59
- end
9
+ Exwiw::SchemaGenerator.from_rails_application(
10
+ output_dir: ENV["OUTPUT_DIR_PATH"] || "exwiw",
11
+ ).generate!
60
12
  end
61
13
  end
62
14
  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.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia
@@ -32,22 +32,28 @@ executables:
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".rubocop.yml"
35
36
  - CHANGELOG.md
36
37
  - LICENSE.txt
37
38
  - README.md
38
39
  - exe/exwiw
39
40
  - lib/exwiw.rb
40
41
  - lib/exwiw/adapter.rb
42
+ - lib/exwiw/adapter/mongodb_adapter.rb
41
43
  - lib/exwiw/adapter/mysql2_adapter.rb
42
44
  - lib/exwiw/adapter/postgresql_adapter.rb
43
45
  - lib/exwiw/adapter/sqlite3_adapter.rb
44
46
  - lib/exwiw/belongs_to.rb
45
47
  - lib/exwiw/cli.rb
46
48
  - lib/exwiw/determine_table_processing_order.rb
49
+ - lib/exwiw/mongo_query.rb
50
+ - lib/exwiw/mongodb_collection_config.rb
51
+ - lib/exwiw/mongodb_field.rb
47
52
  - lib/exwiw/query_ast.rb
48
53
  - lib/exwiw/query_ast_builder.rb
49
54
  - lib/exwiw/railtie.rb
50
55
  - lib/exwiw/runner.rb
56
+ - lib/exwiw/schema_generator.rb
51
57
  - lib/exwiw/table_column.rb
52
58
  - lib/exwiw/table_config.rb
53
59
  - lib/exwiw/version.rb