exwiw 0.3.1 → 0.3.3

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: 55e0eecbd5d7117c263f00fb43c36e2bcc31c75eb4b7ef0255402bec2ac108dc
4
- data.tar.gz: '0913f0804ad33023661b947f88cc86adfeb98ef6b047a93f0ba1f09cea52cec9'
3
+ metadata.gz: 4b10da211ee91b4daa986b71b18fce7ac9a782da81ef92a1146f552368b84d66
4
+ data.tar.gz: 3893a581e2dac519021d6c86756333b980b3c587efcc5d13337e0fe98d9aaa42
5
5
  SHA512:
6
- metadata.gz: bb152c10da5489d005660f458ee8cc526a199a15618e41a625694df6d8cca5df623916c14996f03df1ee969b1c51e43b12fc1b7a8676f2754ae01c7211deb251
7
- data.tar.gz: 55ae136ec956f3a3e15d522e3d71388765ab206911e5662e653522d35e3126fb1d67711051bfd19d8cc2a7457194f84065ecc49dde38d7faaca67fddfb70da1f
6
+ metadata.gz: 395b24342f9a752d5cda207a959ba905a9fc57c8b693faa838bf2978b7b1b08654e0c5a9901b8359d9af96afc70b36f5b199cfec04165ef9e6ad9b25df0f3ea6
7
+ data.tar.gz: fb5514dd6d9c724e1d136cf54abe31616f8cec9944522dd7385fbbb04ddf82f9c290c2935d9c7a55949c86b9c433df02fc43f63aa3cff6d93e3f995a07b38f76
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.3] - 2026-05-31
6
+
7
+ ## [0.3.2] - 2026-05-31
8
+
9
+ ### Changed
10
+
11
+ - Adapter names are now driver-agnostic: `--adapter=mysql` and `--adapter=sqlite` replace the driver-flavored `mysql2` / `sqlite3` as the canonical names (`postgresql` and `mongodb` are unchanged). The CLI stays backward compatible — `mysql2` and `sqlite3` are accepted as aliases and folded onto the canonical name, and the matching is case-insensitive so a Rails app's `connection.adapter_name` (e.g. `Mysql2`, `SQLite`) is absorbed too. There is intentionally no `trilogy` adapter *name*: the MySQL adapter now connects through whichever of the `mysql2` or `trilogy` gem is installed (preferring `mysql2`), so an app on either driver works by passing `--adapter=mysql`. trilogy's typed result values are normalized back to the same raw strings `mysql2`'s `cast: false` returns, so the generated SQL is identical regardless of driver. **Breaking** only for code using the library classes directly: `Exwiw::Adapter::Mysql2Adapter` → `Exwiw::Adapter::MysqlAdapter` and `Exwiw::Adapter::Sqlite3Adapter` → `Exwiw::Adapter::SqliteAdapter`. The `EXWIW_DATABASE_ADAPTER` value passed to `--after-insert-hook` is now the canonical name (`mysql` / `sqlite`).
12
+
5
13
  ## [0.3.1] - 2026-05-31
6
14
 
7
15
  ### Changed
data/README.md CHANGED
@@ -37,11 +37,23 @@ gem install exwiw
37
37
 
38
38
  ## Supported Databases
39
39
 
40
- - mysql2
40
+ - mysql
41
41
  - postgresql
42
- - sqlite3
42
+ - sqlite
43
43
  - mongodb (experimental, see [MongoDB notes](#mongodb-notes))
44
44
 
45
+ For MySQL, exwiw connects through whichever of the `mysql2` or `trilogy` gem is
46
+ available (preferring `mysql2`), so an app on either driver works without any
47
+ extra setup. There is no separate `trilogy` adapter name — pass `--adapter=mysql`
48
+ either way.
49
+
50
+ Set `EXWIW_MYSQL_DRIVER=trilogy` (or `mysql2`) to force a specific driver. This
51
+ is useful when the `mysql2` gem is linked against a `libmysqlclient` that can no
52
+ longer load the server's auth plugin — e.g. a MySQL 9.x client drops the
53
+ `mysql_native_password` plugin and raises `Authentication plugin
54
+ 'mysql_native_password' cannot be loaded` on connect. The pure-Ruby `trilogy`
55
+ driver implements that auth handshake itself and sidesteps the issue.
56
+
45
57
  ## Usage
46
58
 
47
59
  exwiw has two subcommands:
@@ -55,7 +67,7 @@ exwiw has two subcommands:
55
67
  # dump & masking all records from database to dump.sql based on schema.json
56
68
  # pass database password as an environment variable 'DATABASE_PASSWORD'
57
69
  exwiw \
58
- --adapter=mysql2 \
70
+ --adapter=mysql \
59
71
  --host=localhost \
60
72
  --port=3306 \
61
73
  --user=reader \
@@ -92,7 +104,7 @@ This command will generate sql files in the `dump` directory.
92
104
  idx means the order of the dump. bigger idx might depend on smaller idx,
93
105
  so you should import the dump in order.
94
106
 
95
- `insert-000-schema.sql` is generated by shelling out to the database client tools (`mysqldump` for `mysql2`, `pg_dump` for `postgresql`, and the sqlite3 driver for `sqlite3`), so the corresponding client must be available on PATH when running exwiw. The output is post-processed to make it idempotent: `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` (where the engine supports it), and PostgreSQL's `ALTER TABLE ... ADD CONSTRAINT` statements are wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object`.
107
+ `insert-000-schema.sql` is generated by shelling out to the database client tools (`mysqldump` for `mysql`, `pg_dump` for `postgresql`, and the sqlite3 driver for `sqlite`), so the corresponding client must be available on PATH when running exwiw. For `mysql`, set `EXWIW_MYSQLDUMP` to point at a specific `mysqldump` binary when the one on PATH is incompatible with the server (e.g. a MySQL 9.x `mysqldump` cannot load `mysql_native_password` against a server still using that auth plugin — `EXWIW_MYSQLDUMP=/path/to/mysql@8.0/bin/mysqldump`). The output is post-processed to make it idempotent: `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` (where the engine supports it), and PostgreSQL's `ALTER TABLE ... ADD CONSTRAINT` statements are wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object`.
96
108
 
97
109
  you need to delete the records before importing the dump,
98
110
  `delete-{idx}-{table_name}.sql` will help you to do that.
@@ -101,7 +113,7 @@ idx meaning is the same as insert sql.
101
113
 
102
114
  ### `exwiw explain`
103
115
 
104
- Print the compiled SQL and its `EXPLAIN` output (estimate-only; `EXPLAIN QUERY PLAN` on SQLite) for each query that `dump` would run, to stdout. No SELECT is executed. Supported for `mysql2`, `postgresql`, and `sqlite3`. The `mongodb` adapter is not yet supported.
116
+ Print the compiled SQL and its `EXPLAIN` output (estimate-only; `EXPLAIN QUERY PLAN` on SQLite) for each query that `export` would run, to stdout. No SELECT is executed. Supported for `mysql`, `postgresql`, and `sqlite`. The `mongodb` adapter is not yet supported.
105
117
 
106
118
  ```bash
107
119
  # preview the queries exwiw would run, without executing the SELECTs
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Exwiw
4
4
  module Adapter
5
- class Mysql2Adapter < Base
5
+ class MysqlAdapter < Base
6
6
  def build_query(table, dump_target, table_by_name)
7
7
  Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
8
8
  end
@@ -11,17 +11,17 @@ module Exwiw
11
11
  sql = commented_sql(query_ast)
12
12
 
13
13
  @logger.debug(" Executing SQL: \n#{sql}")
14
- connection.query(sql, cast: false, as: :array).to_a
14
+ connection.query(sql).rows
15
15
  end
16
16
 
17
17
  def explain(query_ast)
18
18
  sql = commented_sql(query_ast)
19
19
 
20
20
  @logger.debug(" Executing EXPLAIN: \n#{sql}")
21
- rows = connection.query("EXPLAIN #{sql}", cast: false).to_a
22
- rows.each_with_index.flat_map do |row, i|
21
+ result = connection.query("EXPLAIN #{sql}")
22
+ result.rows.each_with_index.flat_map do |row, i|
23
23
  ["*************************** #{i + 1}. row ***************************"] +
24
- row.map { |k, v| "#{k}: #{v}" }
24
+ result.fields.zip(row).map { |k, v| "#{k}: #{v}" }
25
25
  end.join("\n")
26
26
  end
27
27
 
@@ -34,8 +34,16 @@ module Exwiw
34
34
  return
35
35
  end
36
36
 
37
+ # The mysqldump binary is invoked directly (not via the mysql2/trilogy
38
+ # driver), so point EXWIW_MYSQLDUMP at a specific binary when the one on
39
+ # PATH is incompatible with the server — e.g. a MySQL 9.x client whose
40
+ # mysqldump cannot load `mysql_native_password` ("plugin ... cannot be
41
+ # loaded", exit 2) against a server still using that auth plugin.
42
+ mysqldump_bin = ENV['EXWIW_MYSQLDUMP']
43
+ mysqldump_bin = 'mysqldump' if mysqldump_bin.nil? || mysqldump_bin.empty?
44
+
37
45
  cmd = [
38
- 'mysqldump',
46
+ mysqldump_bin,
39
47
  "--host=#{@connection_config.host}",
40
48
  "--port=#{@connection_config.port}",
41
49
  "--user=#{@connection_config.user}",
@@ -58,11 +66,18 @@ module Exwiw
58
66
  ]
59
67
  env = { 'MYSQL_PWD' => @connection_config.password.to_s }
60
68
 
61
- @logger.debug(" Running mysqldump for #{table_names.size} table(s)...")
62
- stdout, stderr, status = Open3.capture3(env, *cmd)
69
+ @logger.debug(" Running #{mysqldump_bin} for #{table_names.size} table(s)...")
70
+ stdout, stderr, status =
71
+ begin
72
+ Open3.capture3(env, *cmd)
73
+ rescue Errno::ENOENT
74
+ raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \
75
+ "or set EXWIW_MYSQLDUMP to a mysqldump binary."
76
+ end
63
77
  unless status.success?
64
78
  if stderr.include?('command not found') || stderr.empty?
65
- raise "Failed to run `mysqldump`. Ensure the mysql client is installed and on PATH. stderr: #{stderr}"
79
+ raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \
80
+ "or set EXWIW_MYSQLDUMP to a mysqldump binary. stderr: #{stderr}"
66
81
  end
67
82
  raise "mysqldump failed (exit #{status.exitstatus}): #{stderr}"
68
83
  end
@@ -257,17 +272,7 @@ module Exwiw
257
272
  end
258
273
 
259
274
  private def connection
260
- @connection ||=
261
- begin
262
- require 'mysql2'
263
- Mysql2::Client.new(
264
- host: @connection_config.host,
265
- port: @connection_config.port,
266
- username: @connection_config.user,
267
- password: @connection_config.password,
268
- database: @connection_config.database_name
269
- )
270
- end
275
+ @connection ||= MysqlClient.new(@connection_config)
271
276
  end
272
277
  end
273
278
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Exwiw
6
+ module Adapter
7
+ # Thin wrapper over the MySQL driver so MysqlAdapter does not care whether the
8
+ # host app ships the `mysql2` gem or the `trilogy` gem. exwiw only runs simple
9
+ # SELECT / EXPLAIN queries, so both drivers are normalized to the same shape:
10
+ # rows as arrays of String|nil plus the column names.
11
+ #
12
+ # Values are normalized to strings to match mysql2's `cast: false` mode, where
13
+ # every column comes back as a raw string and is quoted uniformly downstream
14
+ # (see MysqlAdapter#escape_value). mysql2 already returns strings in that mode;
15
+ # trilogy always casts to Ruby types (Integer / BigDecimal / Time / Date / ...),
16
+ # so its values are stringified back into the same literal form here.
17
+ class MysqlClient
18
+ # Immutable value object: a query's column names and its rows.
19
+ Result = Data.define(:fields, :rows)
20
+
21
+ DRIVERS = [:mysql2, :trilogy].freeze
22
+
23
+ # Pick the available driver, preferring mysql2 (exwiw's historical default).
24
+ # require returns false when already loaded, so this is safe to call repeatedly.
25
+ #
26
+ # Set EXWIW_MYSQL_DRIVER=trilogy to force the pure-Ruby trilogy driver. This
27
+ # is useful when the mysql2 gem is linked against a libmysqlclient that can
28
+ # no longer load the server's auth plugin (e.g. MySQL 9.x client dropped the
29
+ # `mysql_native_password` plugin .so, raising "Authentication plugin
30
+ # 'mysql_native_password' cannot be loaded" on connect).
31
+ def self.detect_driver
32
+ forced = ENV['EXWIW_MYSQL_DRIVER']
33
+ if forced && !forced.empty?
34
+ sym = forced.to_sym
35
+ unless DRIVERS.include?(sym)
36
+ raise ArgumentError,
37
+ "EXWIW_MYSQL_DRIVER must be one of #{DRIVERS.join(', ')}, got #{forced.inspect}."
38
+ end
39
+ return sym
40
+ end
41
+
42
+ require 'mysql2'
43
+ :mysql2
44
+ rescue LoadError
45
+ begin
46
+ require 'trilogy'
47
+ :trilogy
48
+ rescue LoadError
49
+ raise LoadError,
50
+ "exwiw needs the 'mysql2' or 'trilogy' gem to connect to MySQL. " \
51
+ "Add `gem \"mysql2\"` (or `gem \"trilogy\"`) to your Gemfile."
52
+ end
53
+ end
54
+
55
+ # Render a driver-returned value as the raw string mysql2's `cast: false`
56
+ # would have produced, so trilogy's typed values quote identically.
57
+ def self.stringify_value(value)
58
+ case value
59
+ when nil then nil
60
+ when String then value
61
+ when Time
62
+ # Emit fractional seconds only when present. A Time can't tell us the
63
+ # column's declared precision, so a zero fraction on a DATETIME(6)
64
+ # column comes out as "...:00" here whereas mysql2's `cast: false`
65
+ # echoes the raw "...:00.000000"; both re-insert to the same instant.
66
+ value.nsec.zero? ? value.strftime('%Y-%m-%d %H:%M:%S') : value.strftime('%Y-%m-%d %H:%M:%S.%6N')
67
+ when Date then value.strftime('%Y-%m-%d')
68
+ when true then '1'
69
+ when false then '0'
70
+ else
71
+ if defined?(BigDecimal) && value.is_a?(BigDecimal)
72
+ value.to_s('F')
73
+ else
74
+ value.to_s
75
+ end
76
+ end
77
+ end
78
+
79
+ attr_reader :driver
80
+
81
+ # `driver:` is mainly a test seam to force a specific driver; in normal use
82
+ # it is auto-detected.
83
+ def initialize(connection_config, driver: nil)
84
+ @connection_config = connection_config
85
+ @driver = (driver || self.class.detect_driver).to_sym
86
+ ensure_driver_loaded!
87
+ end
88
+
89
+ # @param sql [String]
90
+ # @return [Result] fields (Array<String>) and rows (Array<Array<String|nil>>)
91
+ def query(sql)
92
+ case @driver
93
+ when :mysql2
94
+ res = raw.query(sql, cast: false, as: :array)
95
+ Result.new(res.fields, res.to_a)
96
+ when :trilogy
97
+ res = raw.query(sql)
98
+ rows = res.rows.map { |row| row.map { |value| self.class.stringify_value(value) } }
99
+ Result.new(res.fields, rows)
100
+ else
101
+ raise "Unsupported MySQL driver: #{@driver.inspect}"
102
+ end
103
+ end
104
+
105
+ private def ensure_driver_loaded!
106
+ case @driver
107
+ when :mysql2 then require 'mysql2'
108
+ when :trilogy then require 'trilogy'
109
+ else raise "Unsupported MySQL driver: #{@driver.inspect}"
110
+ end
111
+ end
112
+
113
+ private def raw
114
+ @raw ||= build_raw
115
+ end
116
+
117
+ private def build_raw
118
+ options = {
119
+ host: @connection_config.host,
120
+ port: @connection_config.port&.to_i,
121
+ username: @connection_config.user,
122
+ password: @connection_config.password,
123
+ database: @connection_config.database_name,
124
+ }.compact
125
+
126
+ case @driver
127
+ when :mysql2 then Mysql2::Client.new(options)
128
+ when :trilogy then Trilogy.new(options)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Exwiw
4
4
  module Adapter
5
- class Sqlite3Adapter < Base
5
+ class SqliteAdapter < Base
6
6
  def build_query(table, dump_target, table_by_name)
7
7
  Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
8
8
  end
@@ -53,7 +53,7 @@ module Exwiw
53
53
  end
54
54
 
55
55
  File.open(output_path, 'w') do |file|
56
- file.puts("-- Auto-generated by exwiw. Idempotent CREATE statements for sqlite3.")
56
+ file.puts("-- Auto-generated by exwiw. Idempotent CREATE statements for SQLite.")
57
57
  file.puts(statements.join("\n"))
58
58
  end
59
59
  @logger.info(" Wrote #{statements.size} schema statement(s) to #{output_path}.")
data/lib/exwiw/adapter.rb CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  module Exwiw
4
4
  module Adapter
5
+ # The adapter names exwiw uses internally. Deliberately driver-agnostic
6
+ # (e.g. `mysql`, not `mysql2`) so they describe the database, not the Ruby
7
+ # driver or Rails ActiveRecord adapter that happens to talk to it.
8
+ CANONICAL_ADAPTERS = %w[mysql sqlite postgresql mongodb].freeze
9
+
10
+ # Maps the older driver-flavored spellings onto exwiw's canonical adapter
11
+ # name, so the CLI stays backward compatible (`--adapter=mysql2` still works)
12
+ # and a Rails app's `connection.adapter_name` (e.g. "Mysql2", "SQLite") is
13
+ # absorbed — lookup is case-insensitive (see .normalize_name).
14
+ #
15
+ # NOTE: this only normalizes the *name*; exwiw always connects with the
16
+ # `mysql2` gem (see MysqlAdapter#connection). It is deliberately not aliased
17
+ # from `trilogy`: a source app using the trilogy driver still needs the
18
+ # `mysql2` gem available for exwiw to connect, so accepting `--adapter=trilogy`
19
+ # would falsely imply trilogy support. Use `--adapter=mysql` in that case.
20
+ ADAPTER_ALIASES = {
21
+ "mysql2" => "mysql",
22
+ "sqlite3" => "sqlite",
23
+ }.freeze
24
+
25
+ # Normalize an adapter name to its canonical form. Unknown names are passed
26
+ # through (downcased) so the caller can reject them with a clear message.
27
+ # Returns nil for nil input.
28
+ def self.normalize_name(name)
29
+ return nil if name.nil?
30
+
31
+ key = name.to_s.downcase
32
+ ADAPTER_ALIASES.fetch(key, key)
33
+ end
34
+
5
35
  class Base
6
36
  attr_reader :connection_config
7
37
 
@@ -129,17 +159,17 @@ module Exwiw
129
159
  end
130
160
 
131
161
  def self.build(connection_config, logger)
132
- case connection_config.adapter
133
- when 'sqlite3'
134
- Adapter::Sqlite3Adapter.new(connection_config, logger)
135
- when 'mysql2'
136
- Adapter::Mysql2Adapter.new(connection_config, logger)
162
+ case normalize_name(connection_config.adapter)
163
+ when 'sqlite'
164
+ Adapter::SqliteAdapter.new(connection_config, logger)
165
+ when 'mysql'
166
+ Adapter::MysqlAdapter.new(connection_config, logger)
137
167
  when 'postgresql'
138
168
  Adapter::PostgresqlAdapter.new(connection_config, logger)
139
169
  when 'mongodb'
140
170
  Adapter::MongodbAdapter.new(connection_config, logger)
141
171
  else
142
- raise 'Unsupported adapter'
172
+ raise "Unsupported adapter: #{connection_config.adapter.inspect}"
143
173
  end
144
174
  end
145
175
  end
data/lib/exwiw/cli.rb CHANGED
@@ -99,6 +99,19 @@ module Exwiw
99
99
  end
100
100
 
101
101
  private def validate_options!
102
+ # Fold driver/Rails adapter spellings (mysql2, sqlite3) into exwiw's
103
+ # canonical names up front, so every check below — and the
104
+ # EXWIW_DATABASE_ADAPTER passed to hooks — sees the canonical name.
105
+ @database_adapter = Adapter.normalize_name(@database_adapter)
106
+
107
+ # Reject an unknown adapter up front, before checking adapter-specific
108
+ # options like host/port/password, so the error points at the real problem.
109
+ unless Adapter::CANONICAL_ADAPTERS.include?(@database_adapter)
110
+ $stderr.puts "Invalid adapter. Available options are: #{Adapter::CANONICAL_ADAPTERS.join(', ')} " \
111
+ "(aliases also accepted: #{Adapter::ADAPTER_ALIASES.keys.join(', ')})"
112
+ exit 1
113
+ end
114
+
102
115
  resolve_target_collection_alias!
103
116
  resolve_ids_column_alias!
104
117
 
@@ -106,7 +119,7 @@ module Exwiw
106
119
  validate_explain_only!
107
120
  end
108
121
 
109
- if @database_adapter != "sqlite3"
122
+ if @database_adapter != "sqlite"
110
123
  required_options = {
111
124
  "Target database host" => @database_host,
112
125
  "Target database port" => @database_port,
@@ -126,12 +139,6 @@ module Exwiw
126
139
  end
127
140
  end
128
141
 
129
- valid_adapters = ["mysql2", "postgresql", "sqlite3", "mongodb"]
130
- unless valid_adapters.include?(@database_adapter)
131
- $stderr.puts "Invalid adapter. Available options are: #{valid_adapters.join(', ')}"
132
- exit 1
133
- end
134
-
135
142
  if @subcommand == "export"
136
143
  @output_dir ||= "dump"
137
144
  @output_format ||= "insert"
@@ -227,7 +234,7 @@ module Exwiw
227
234
  end
228
235
 
229
236
  if @ids_column
230
- sql_adapters = ["mysql2", "postgresql", "sqlite3"]
237
+ sql_adapters = ["mysql", "postgresql", "sqlite"]
231
238
  unless sql_adapters.include?(@database_adapter)
232
239
  $stderr.puts "--ids-column is only supported by the sql adapters (use --ids-field)"
233
240
  exit 1
@@ -323,7 +330,7 @@ module Exwiw
323
330
  v = v.end_with?("/") ? v[0..-2] : v
324
331
  @config_dir = File.expand_path(v)
325
332
  end
326
- opts.on("-a", "--adapter=ADAPTER", "Database adapter") { |v| @database_adapter = v }
333
+ opts.on("-a", "--adapter=ADAPTER", "Database adapter: mysql, sqlite, postgresql, mongodb (aliases: mysql2, sqlite3)") { |v| @database_adapter = v }
327
334
  opts.on("--database=DATABASE", "Target database name") { |v| @database_name = v }
328
335
  opts.on("--target-table=[TABLE]", "Target table for extraction. If omitted, dump all tables.") { |v| @target_table_name = v }
329
336
  opts.on("--target-collection=[COLLECTION]", "Alias of --target-table for the mongodb adapter.") { |v| @target_collection_name = v }
@@ -24,6 +24,10 @@ module Exwiw
24
24
  not_resolved_names.empty?
25
25
  end
26
26
 
27
+ if tables_with_no_dependencies.empty?
28
+ raise ArgumentError, build_cycle_error_message(table_by_name, ordered_table_names)
29
+ end
30
+
27
31
  tables_with_no_dependencies.each do |table|
28
32
  ordered_table_names << table.name
29
33
  table_by_name.delete(table.name)
@@ -38,5 +42,25 @@ module Exwiw
38
42
  acc << relation.table_name
39
43
  end
40
44
  end
45
+
46
+ # When no table can be resolved but some remain, the belongs_to graph
47
+ # contains a cycle (e.g. A belongs_to B and B belongs_to A). A topological
48
+ # order cannot exist, so report the offending tables instead of looping
49
+ # forever.
50
+ private_class_method def cycle_diagnostics(table_by_name, ordered_table_names)
51
+ table_by_name.values.map do |table|
52
+ unresolved = (compute_table_dependencies(table) - ordered_table_names - [table.name]).uniq
53
+ " #{table.name} -> #{unresolved.join(', ')}"
54
+ end
55
+ end
56
+
57
+ private_class_method def build_cycle_error_message(table_by_name, ordered_table_names)
58
+ "Circular belongs_to dependency detected among tables: " \
59
+ "#{table_by_name.keys.sort.join(', ')}. " \
60
+ "A processing order cannot be determined. " \
61
+ "Remove one of the belongs_to entries forming the cycle.\n" \
62
+ "Unresolved dependencies:\n" \
63
+ "#{cycle_diagnostics(table_by_name, ordered_table_names).join("\n")}"
64
+ end
41
65
  end
42
66
  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.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
data/lib/exwiw.rb CHANGED
@@ -13,8 +13,9 @@ require_relative "exwiw/mongodb_field"
13
13
  require_relative "exwiw/mongodb_collection_config"
14
14
  require_relative "exwiw/ddl_postprocessor"
15
15
  require_relative "exwiw/adapter"
16
- require_relative "exwiw/adapter/sqlite3_adapter"
17
- require_relative "exwiw/adapter/mysql2_adapter"
16
+ require_relative "exwiw/adapter/sqlite_adapter"
17
+ require_relative "exwiw/adapter/mysql_client"
18
+ require_relative "exwiw/adapter/mysql_adapter"
18
19
  require_relative "exwiw/adapter/postgresql_adapter"
19
20
  require_relative "exwiw/adapter/mongodb_adapter"
20
21
  require_relative "exwiw/determine_table_processing_order"
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.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia
@@ -45,9 +45,10 @@ files:
45
45
  - lib/exwiw.rb
46
46
  - lib/exwiw/adapter.rb
47
47
  - lib/exwiw/adapter/mongodb_adapter.rb
48
- - lib/exwiw/adapter/mysql2_adapter.rb
48
+ - lib/exwiw/adapter/mysql_adapter.rb
49
+ - lib/exwiw/adapter/mysql_client.rb
49
50
  - lib/exwiw/adapter/postgresql_adapter.rb
50
- - lib/exwiw/adapter/sqlite3_adapter.rb
51
+ - lib/exwiw/adapter/sqlite_adapter.rb
51
52
  - lib/exwiw/after_insert_hook.rb
52
53
  - lib/exwiw/belongs_to.rb
53
54
  - lib/exwiw/cli.rb