exwiw 0.3.1 → 0.3.2

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: 83bb3c7245d9174e5d16e85e6de2a530b58ca445b570d692c5198682fc43b9ef
4
+ data.tar.gz: cee17f919dab949c82942eae146a1d6182909ab6ef595e8cfc7d4c3f744d1ede
5
5
  SHA512:
6
- metadata.gz: bb152c10da5489d005660f458ee8cc526a199a15618e41a625694df6d8cca5df623916c14996f03df1ee969b1c51e43b12fc1b7a8676f2754ae01c7211deb251
7
- data.tar.gz: 55ae136ec956f3a3e15d522e3d71388765ab206911e5662e653522d35e3126fb1d67711051bfd19d8cc2a7457194f84065ecc49dde38d7faaca67fddfb70da1f
6
+ metadata.gz: d655b0e5d55759932d7ab6646cb9ed9d0b699e6c96e6061863091eded7914c736b74c558e8a96d132ea94ef668376ae372a9eacf23f978d52540bf31c38c7c18
7
+ data.tar.gz: 5e068572f70d2b1869e5dffceba9f681d0b153c0947da3fd7b35d746e7f33bbc6c2d5fa25d23c9dfa0f190e14d3304ec6f2940aa19168749aa05b303c572ea10
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.2] - 2026-05-31
6
+
7
+ ### Changed
8
+
9
+ - 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`).
10
+
5
11
  ## [0.3.1] - 2026-05-31
6
12
 
7
13
  ### Changed
data/README.md CHANGED
@@ -37,11 +37,27 @@ 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
+ Adapter names are driver-agnostic — they name the database, not the Ruby driver
46
+ or Rails ActiveRecord adapter. For backward compatibility and to absorb the Rails
47
+ adapter spelling, the following aliases are also accepted (case-insensitive):
48
+
49
+ | `--adapter` value | Aliases accepted |
50
+ | --- | --- |
51
+ | `mysql` | `mysql2` |
52
+ | `sqlite` | `sqlite3` |
53
+
54
+ So `--adapter=mysql2` and `--adapter=mysql` both select the same MySQL adapter.
55
+
56
+ For MySQL, exwiw connects through whichever of the `mysql2` or `trilogy` gem is
57
+ available (preferring `mysql2`), so an app on either driver works without any
58
+ extra setup. There is no separate `trilogy` adapter name — pass `--adapter=mysql`
59
+ either way.
60
+
45
61
  ## Usage
46
62
 
47
63
  exwiw has two subcommands:
@@ -55,7 +71,7 @@ exwiw has two subcommands:
55
71
  # dump & masking all records from database to dump.sql based on schema.json
56
72
  # pass database password as an environment variable 'DATABASE_PASSWORD'
57
73
  exwiw \
58
- --adapter=mysql2 \
74
+ --adapter=mysql \
59
75
  --host=localhost \
60
76
  --port=3306 \
61
77
  --user=reader \
@@ -92,7 +108,7 @@ This command will generate sql files in the `dump` directory.
92
108
  idx means the order of the dump. bigger idx might depend on smaller idx,
93
109
  so you should import the dump in order.
94
110
 
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`.
111
+ `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. 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
112
 
97
113
  you need to delete the records before importing the dump,
98
114
  `delete-{idx}-{table_name}.sql` will help you to do that.
@@ -101,7 +117,7 @@ idx meaning is the same as insert sql.
101
117
 
102
118
  ### `exwiw explain`
103
119
 
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.
120
+ 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
121
 
106
122
  ```bash
107
123
  # 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
 
@@ -257,17 +257,7 @@ module Exwiw
257
257
  end
258
258
 
259
259
  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
260
+ @connection ||= MysqlClient.new(@connection_config)
271
261
  end
272
262
  end
273
263
  end
@@ -0,0 +1,115 @@
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
+ # Pick the available driver, preferring mysql2 (exwiw's historical default).
22
+ # require returns false when already loaded, so this is safe to call repeatedly.
23
+ def self.detect_driver
24
+ require 'mysql2'
25
+ :mysql2
26
+ rescue LoadError
27
+ begin
28
+ require 'trilogy'
29
+ :trilogy
30
+ rescue LoadError
31
+ raise LoadError,
32
+ "exwiw needs the 'mysql2' or 'trilogy' gem to connect to MySQL. " \
33
+ "Add `gem \"mysql2\"` (or `gem \"trilogy\"`) to your Gemfile."
34
+ end
35
+ end
36
+
37
+ # Render a driver-returned value as the raw string mysql2's `cast: false`
38
+ # would have produced, so trilogy's typed values quote identically.
39
+ def self.stringify_value(value)
40
+ case value
41
+ when nil then nil
42
+ when String then value
43
+ when Time
44
+ # Emit fractional seconds only when present. A Time can't tell us the
45
+ # column's declared precision, so a zero fraction on a DATETIME(6)
46
+ # column comes out as "...:00" here whereas mysql2's `cast: false`
47
+ # echoes the raw "...:00.000000"; both re-insert to the same instant.
48
+ value.nsec.zero? ? value.strftime('%Y-%m-%d %H:%M:%S') : value.strftime('%Y-%m-%d %H:%M:%S.%6N')
49
+ when Date then value.strftime('%Y-%m-%d')
50
+ when true then '1'
51
+ when false then '0'
52
+ else
53
+ if defined?(BigDecimal) && value.is_a?(BigDecimal)
54
+ value.to_s('F')
55
+ else
56
+ value.to_s
57
+ end
58
+ end
59
+ end
60
+
61
+ attr_reader :driver
62
+
63
+ # `driver:` is mainly a test seam to force a specific driver; in normal use
64
+ # it is auto-detected.
65
+ def initialize(connection_config, driver: nil)
66
+ @connection_config = connection_config
67
+ @driver = (driver || self.class.detect_driver).to_sym
68
+ ensure_driver_loaded!
69
+ end
70
+
71
+ # @param sql [String]
72
+ # @return [Result] fields (Array<String>) and rows (Array<Array<String|nil>>)
73
+ def query(sql)
74
+ case @driver
75
+ when :mysql2
76
+ res = raw.query(sql, cast: false, as: :array)
77
+ Result.new(res.fields, res.to_a)
78
+ when :trilogy
79
+ res = raw.query(sql)
80
+ rows = res.rows.map { |row| row.map { |value| self.class.stringify_value(value) } }
81
+ Result.new(res.fields, rows)
82
+ else
83
+ raise "Unsupported MySQL driver: #{@driver.inspect}"
84
+ end
85
+ end
86
+
87
+ private def ensure_driver_loaded!
88
+ case @driver
89
+ when :mysql2 then require 'mysql2'
90
+ when :trilogy then require 'trilogy'
91
+ else raise "Unsupported MySQL driver: #{@driver.inspect}"
92
+ end
93
+ end
94
+
95
+ private def raw
96
+ @raw ||= build_raw
97
+ end
98
+
99
+ private def build_raw
100
+ options = {
101
+ host: @connection_config.host,
102
+ port: @connection_config.port&.to_i,
103
+ username: @connection_config.user,
104
+ password: @connection_config.password,
105
+ database: @connection_config.database_name,
106
+ }.compact
107
+
108
+ case @driver
109
+ when :mysql2 then Mysql2::Client.new(options)
110
+ when :trilogy then Trilogy.new(options)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ 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 }
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.2"
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.2
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