exwiw 0.1.8 → 0.1.9

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: 7c5e29a492af74dfbfa0e778fcb527a218d3a33507024646b2fe88b495581c2f
4
- data.tar.gz: bd66516a56f40e4147e76e3fc662c98cd3c0261eb54a310ef7af49bcc5373cf0
3
+ metadata.gz: b48628d0a7599b151f957d6a40cd5a23cc3befa5007a86c550c834878b9893cc
4
+ data.tar.gz: 38fe41ada0a3e0a8f358bd60c3a28a7b7637e91bd2066d2e2a97e5542716540a
5
5
  SHA512:
6
- metadata.gz: 1b63d52ce0abd624695b73d782c64a5d4dd861e701f7c5989e977dd506d0178f4e2d394e9ae57e1106bf83898b622bd93681c60bdfbbde7dcd72bf1796cd4847
7
- data.tar.gz: 36ea50859424c45eb3bd83ffc84fb148eb4080345e7dfc012ff81b32003fa27e83f43324c4e7fa296c4d35ee8de6f89754a9e9f1449bd69d3d1b4066015e51bc
6
+ metadata.gz: 4105dbad3eb0291b841e7ebf152776913e19d817dbf3a9901a6838d7f16113c4fbd30887b88015fed735c0ec4b82c03367fafed81f2f6c7669c8f091da9b4041
7
+ data.tar.gz: 28a6383dbc953b93772f46adaf488cb4f0e826a0642859700c48536de46bde633a55780d8baff588da3e81dd6c4534d2fedd355424f7b68a70082c462eaf59aa
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.9] - 2026-05-21
6
+
7
+ Added
8
+
9
+ - PostgreSQL: --output-format=copy — emits COPY FROM stdin blocks instead of
10
+ INSERT statements. Faster restores for large dumps. (c026960)
11
+ - PostgreSQL: dump-all-tables mode — running without --target-table / --ids now
12
+ dumps every table in the scenario. (c026960)
13
+
14
+ Fixed
15
+
16
+ - PostgreSQL: value escape sequences — corrected escape handling in formatted
17
+ values. (50e6772)
18
+
19
+ https://github.com/heyinc/exwiw/pull/21
20
+
5
21
  ## [0.1.8] - 2026-05-16
6
22
 
7
23
  ### Added
data/README.md CHANGED
@@ -62,6 +62,20 @@ exwiw \
62
62
  --log-level=info
63
63
  ```
64
64
 
65
+ When `--target-table` and `--ids` are omitted, exwiw dumps all tables defined in `--config-dir`:
66
+
67
+ ```bash
68
+ # dump all tables
69
+ exwiw \
70
+ --adapter=postgresql \
71
+ --host=localhost \
72
+ --port=5432 \
73
+ --user=reader \
74
+ --database=app_production \
75
+ --config-dir=exwiw \
76
+ --output-dir=dump
77
+ ```
78
+
65
79
  This command will generate sql files in the `dump` directory.
66
80
 
67
81
  - `dump/insert-000-schema.sql` — idempotent `CREATE TABLE IF NOT EXISTS ...` for every table in scope. Apply this first to provision an empty database.
@@ -120,6 +134,32 @@ This is an example of the one table schema:
120
134
 
121
135
  `--config-dir` will use all json files in the specified directory.
122
136
 
137
+ ### Output format
138
+
139
+ By default, exwiw generates `INSERT` statements. For PostgreSQL, you can use `--output-format=copy` to generate `COPY FROM stdin` format instead, which is significantly faster for bulk loading:
140
+
141
+ ```bash
142
+ exwiw \
143
+ --adapter=postgresql \
144
+ --host=localhost \
145
+ --port=5432 \
146
+ --user=reader \
147
+ --database=app_production \
148
+ --config-dir=exwiw \
149
+ --target-table=shops \
150
+ --ids=1 \
151
+ --output-dir=dump \
152
+ --output-format=copy
153
+ ```
154
+
155
+ The generated file uses tab-separated values with PostgreSQL's text-format escaping (`\N` for NULL, `\\` for backslash, etc.). Import with `psql`:
156
+
157
+ ```bash
158
+ psql -d app_dev -f dump/insert-001-shops.sql
159
+ ```
160
+
161
+ `--output-format=copy` is only supported with the `postgresql` adapter.
162
+
123
163
  ### Bulk insert chunk size
124
164
 
125
165
  `bulk_insert_chunk_size` splits the generated `INSERT` statement into multiple statements, each containing at most the specified number of rows. This is useful when the number of records per table is large enough to hit limits like MySQL's `max_allowed_packet`.
@@ -250,7 +290,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
250
290
 
251
291
  ## Contributing
252
292
 
253
- Bug reports and pull requests are welcome on GitHub at https://github.com/riseshia/exwiw.
293
+ Bug reports and pull requests are welcome on GitHub at https://github.com/heyinc/exwiw.
254
294
 
255
295
  ## License
256
296
 
@@ -74,6 +74,16 @@ module Exwiw
74
74
  "INSERT INTO #{table_name} (#{column_names}) VALUES\n#{values};"
75
75
  end
76
76
 
77
+ def to_copy_from_stdin(results, table)
78
+ column_names = table.columns.map(&:name).join(', ')
79
+ lines = ["COPY #{table.name} (#{column_names}) FROM stdin;"]
80
+ results.each do |row|
81
+ lines << row.map { |v| escape_copy_value(v) }.join("\t")
82
+ end
83
+ lines << '\\.'
84
+ lines.join("\n")
85
+ end
86
+
77
87
  # Transcribe the FROM-side sequence cursor backing `table.primary_key`
78
88
  # onto the import target. Without this, importing into a clean DB leaves
79
89
  # the sequence at 1 while the inserted rows occupy higher IDs, so the
@@ -205,6 +215,21 @@ module Exwiw
205
215
  end
206
216
  end
207
217
 
218
+ private def escape_copy_value(value)
219
+ case value
220
+ when nil
221
+ "\\N"
222
+ when String
223
+ value
224
+ .gsub('\\') { '\\\\' }
225
+ .gsub("\t", '\t')
226
+ .gsub("\n", '\n')
227
+ .gsub("\r", '\r')
228
+ else
229
+ value.to_s
230
+ end
231
+ end
232
+
208
233
  private def escape_single_quote(value)
209
234
  value.gsub("'", "''")
210
235
  end
data/lib/exwiw/adapter.rb CHANGED
@@ -70,6 +70,10 @@ module Exwiw
70
70
  def post_insert_sql(_table)
71
71
  nil
72
72
  end
73
+
74
+ def to_copy_from_stdin(_results, _table)
75
+ raise NotImplementedError, "COPY format is not supported by #{self.class.name}"
76
+ end
73
77
  end
74
78
 
75
79
  # @params [Exwiw::QueryAst] query_ast
data/lib/exwiw/cli.rb CHANGED
@@ -28,6 +28,7 @@ module Exwiw
28
28
  @database_name = nil
29
29
  @target_table_name = nil
30
30
  @ids = []
31
+ @output_format = 'insert'
31
32
  @log_level = :info
32
33
 
33
34
  parser.parse!(@argv)
@@ -60,6 +61,7 @@ module Exwiw
60
61
  output_dir: @output_dir,
61
62
  config_dir: @config_dir,
62
63
  dump_target: dump_target,
64
+ output_format: @output_format,
63
65
  logger: logger,
64
66
  ).run
65
67
  end
@@ -92,6 +94,17 @@ module Exwiw
92
94
  exit 1
93
95
  end
94
96
 
97
+ valid_output_formats = ["insert", "copy"]
98
+ unless valid_output_formats.include?(@output_format)
99
+ $stderr.puts "Invalid output format '#{@output_format}'. Available options are: #{valid_output_formats.join(', ')}"
100
+ exit 1
101
+ end
102
+
103
+ if @output_format == "copy" && @database_adapter != "postgresql"
104
+ $stderr.puts "--output-format=copy is only supported with the postgresql adapter"
105
+ exit 1
106
+ end
107
+
95
108
  if @config_dir.nil?
96
109
  $stderr.puts "Config dir is required"
97
110
  exit 1
@@ -107,13 +120,13 @@ module Exwiw
107
120
  exit 1
108
121
  end
109
122
 
110
- if @target_table_name.nil? || @target_table_name.empty?
111
- $stderr.puts "Target table is required"
123
+ if @target_table_name && @ids.empty?
124
+ $stderr.puts "--ids is required when --target-table is specified"
112
125
  exit 1
113
126
  end
114
127
 
115
- if @ids.empty?
116
- $stderr.puts "At least one ID is required"
128
+ if !@target_table_name && @ids.any?
129
+ $stderr.puts "--target-table is required when --ids is specified"
117
130
  exit 1
118
131
  end
119
132
  end
@@ -151,8 +164,9 @@ module Exwiw
151
164
  end
152
165
  opts.on("-a", "--adapter=ADAPTER", "Database adapter") { |v| @database_adapter = v }
153
166
  opts.on("--database=DATABASE", "Target database name") { |v| @database_name = v }
154
- opts.on("--target-table=TABLE", "Target table for extraction") { |v| @target_table_name = v }
155
- opts.on("--ids=IDS", "Comma-separated list of identifiers") { |v| @ids = v.split(',') }
167
+ opts.on("--target-table=[TABLE]", "Target table for extraction. If omitted, dump all tables.") { |v| @target_table_name = v }
168
+ opts.on("--ids=[IDS]", "Comma-separated list of identifiers. Required when --target-table is given.") { |v| @ids = v.split(',') }
169
+ opts.on("--output-format=[FORMAT]", "Output format: insert (default) or copy (PostgreSQL only)") { |v| @output_format = v }
156
170
  opts.on("--log-level=LEVEL", "Log level (debug, info). default is info") { |v| @log_level = v.to_sym }
157
171
 
158
172
  opts.on("--help", "Print this help") do
data/lib/exwiw/runner.rb CHANGED
@@ -9,12 +9,14 @@ module Exwiw
9
9
  output_dir:,
10
10
  config_dir:,
11
11
  dump_target:,
12
- logger:
12
+ logger:,
13
+ output_format: 'insert'
13
14
  )
14
15
  @connection_config = connection_config
15
16
  @output_dir = output_dir
16
17
  @config_dir = config_dir
17
18
  @dump_target = dump_target
19
+ @output_format = output_format
18
20
  @logger = logger
19
21
  end
20
22
 
@@ -52,18 +54,30 @@ module Exwiw
52
54
  @logger.info(" No records matched. skip this table.")
53
55
  next
54
56
  end
55
- @logger.debug(" Generate INSERT statement...")
57
+ insert_idx = (idx + 1).to_s.rjust(3, '0')
56
58
 
57
- chunk_size = table.bulk_insert_chunk_size
58
- chunks = chunk_size ? results.each_slice(chunk_size).to_a : [results]
59
- insert_sql = chunks.map { |chunk_rows| adapter.to_bulk_insert(chunk_rows, table) }.join("\n")
59
+ if @output_format == 'copy'
60
+ @logger.debug(" Generate COPY statement...")
61
+ copy_sql = adapter.to_copy_from_stdin(results, table)
62
+ @logger.info(" Generated COPY statement for #{record_num} records.")
60
63
 
61
- @logger.info(" Generated INSERT statement for #{record_num} records (#{chunks.size} statement(s)).")
62
- insert_idx = (idx + 1).to_s.rjust(3, '0')
63
- File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
64
- file.puts(insert_sql)
65
- post = adapter.post_insert_sql(table)
66
- file.puts(post) if post
64
+ File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
65
+ file.puts(copy_sql)
66
+ post = adapter.post_insert_sql(table)
67
+ file.puts(post) if post
68
+ end
69
+ else
70
+ @logger.debug(" Generate INSERT statement...")
71
+ chunk_size = table.bulk_insert_chunk_size
72
+ chunks = chunk_size ? results.each_slice(chunk_size).to_a : [results]
73
+ insert_sql = chunks.map { |chunk_rows| adapter.to_bulk_insert(chunk_rows, table) }.join("\n")
74
+
75
+ @logger.info(" Generated INSERT statement for #{record_num} records (#{chunks.size} statement(s)).")
76
+ File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
77
+ file.puts(insert_sql)
78
+ post = adapter.post_insert_sql(table)
79
+ file.puts(post) if post
80
+ end
67
81
  end
68
82
 
69
83
  if adapter.supports_bulk_delete?
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.8"
4
+ VERSION = "0.1.9"
5
5
  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.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia
@@ -83,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 4.0.10
86
+ rubygems_version: 3.6.9
87
87
  specification_version: 4
88
88
  summary: Ruby gem that allows you to export records from a database to a dump file.
89
89
  test_files: []