exwiw 0.9.24 → 1.0.0

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: 73740836024ec336feb7138c5a94e396df206de932de00737404d556fc403e0a
4
- data.tar.gz: de53ac3f5e4c93f6761524041261c2becb65baa9bd396e528ca0ffa9b82b518e
3
+ metadata.gz: ecbb0b978dfd64c1e48c396e600662a9f7d4bb9c2c8f0e0050440987b53c815d
4
+ data.tar.gz: eff38c81ce04abb165f0edcad7316e1820d44abd54c6596ce450357c11a9c201
5
5
  SHA512:
6
- metadata.gz: ac457715cf1eaf521840903e2b2fbb6c5f503230d0abfb7cc24510067c2aa68c8b841d370d8eeed3e0c0ee009a105bea87cfe455a21c50b5bd462feb48c14f22
7
- data.tar.gz: 945c4c46077e9ee6678f6fbc6e72b13186e393c4f06035dd1e9d63f10efa3c1fe6adb67a05f470e1330a48d5d8f34d008c67cb8795ba7dbda8294178f13082a0
6
+ metadata.gz: 2b119cb478ae9e6bb019bb2d0d94272ed84f7bb20ac3013236260b8c98433c9c97f37b3d4dafc132a70736d0d6eb4a20e4fc0e44e473dfc5d1a91f7dbda560a0
7
+ data.tar.gz: 17268aa8ec3ad2959521cc4b16d453e86480935ae98ba43e4f38fb4545dada3af23980fc79b8f3751268fa79e078e87c7e7388e8bab7ce9fefbef9e6344c9453
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.0.0] - 2026-09-01
6
+
7
+ ### Removed
8
+
9
+ - **BREAKING — delete SQL support is removed ([#166](https://github.com/heyinc/exwiw/issues/166)): `export` no longer generates `delete-NNN-<table>.sql` files.** They existed to clear an already-populated import target before re-inserting; the supported workflow is now to import into an empty database (`insert-000-schema.sql` provisions one) or to clear the target yourself. Every export now behaves as `--insert-only` always did, so the `--insert-only` flag and the `insert_only:` config key have nothing left to toggle — both are still **accepted but ignored**, with a warning naming them obsolete, so an existing invocation or committed config file keeps working across the upgrade; remove them at your leisure (a future release will reject them). For the adapter API, `to_bulk_delete`, `supports_bulk_delete?`, and `pre_delete_sql` are removed.
10
+
11
+ ### Changed
12
+
13
+ - **BREAKING — PostgreSQL: `insert-000-schema.sql` now contains the source's triggers, wrapped in `DO $exwiw$ ... EXCEPTION WHEN duplicate_object`.** They used to be deleted from the dump, so a restored database silently ran none of them: an audit trigger recorded nothing, a timestamp trigger never fired, and the dump gave whoever restored it no way to add them back. The deletion existed because a `--table` dump emitted `CREATE TRIGGER` without the `CREATE FUNCTION` it references, failing the restore with `PG::UndefinedFunction`; the dump has since become a whole-database one that carries the functions too, so it was a leftover workaround rather than intended behavior — `mysqldump` and sqlite's `sqlite_master` have always emitted triggers. It is nonetheless a breaking change: dumps grow the trigger statements. They do not fire during the load, because each `insert-*.sql` now opens with the `session_replication_role = 'replica'` block described below. The DO block keeps the schema re-appliable, which a bare `CREATE TRIGGER` is not (`CREATE OR REPLACE TRIGGER` is PostgreSQL 14+ only and `pg_dump` never emits it).
14
+
15
+ - **PostgreSQL: each `insert-NNN-<table>.sql` now opens with a `session_replication_role = 'replica'` block, so triggers and foreign keys do not fire while the target is loaded.** With the source's triggers now in the schema, an unguarded load would run every one of them once per inserted row — over rows that already carry the values those triggers produced on the source — and would enforce FKs against a target that is only complete once every `insert-*.sql` has been applied. This is the counterpart of the `FOREIGN_KEY_CHECKS=0` that `MysqlAdapter#pre_insert_sql` has always emitted. Setting the parameter requires superuser (`rds_superuser` on RDS); when the restoring role lacks it the block catches `insufficient_privilege` and downgrades to a `WARNING`, so the load still runs — with triggers firing, as it did before this release. The setting is connection-scoped (`set_config(..., false)`) and is not reset at the end of the file — every file re-arms it itself, so concatenating them works regardless, but sourcing them into a session that goes on to do other work leaves that session in replica mode.
16
+
17
+ ### Fixed
18
+
19
+ - **PostgreSQL: a `CREATE TRIGGER` inside a function body is no longer rewritten.** The removed `DdlPostprocessor.strip_triggers` matched `^CREATE TRIGGER` anywhere, so it also deleted such a line from a dollar-quoted body, leaving a function that still compiled but no longer installed its trigger. Its replacement, `wrap_create_trigger_in_do_block`, anchors on the `-- Name: <table> <trigger>; Type: TRIGGER` header `pg_dump` writes before each trigger, which never appears inside a body. `CREATE CONSTRAINT TRIGGER` is handled, and a `;` inside a quoted trigger argument no longer truncates the statement.
20
+
5
21
  ## [0.9.24] - 2026-08-17
6
22
 
7
23
  ### Fixed
data/README.md CHANGED
@@ -102,12 +102,15 @@ The output dir is emptied before each export so it never mixes files from a prev
102
102
 
103
103
  - `dump/insert-000-schema.sql` — idempotent `CREATE TABLE IF NOT EXISTS ...` for every table in scope. Apply this first to provision an empty database.
104
104
  - `dump/insert-{idx}-{table_name}.sql`
105
- - `dump/delete-{idx}-{table_name}.sql`
106
105
 
107
106
  idx means the order of the dump. bigger idx might depend on smaller idx,
108
107
  so you should import the dump in order.
109
108
 
110
- `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`. For `mysql`, the source server's `DEFINER=user@host` stamp on views and triggers is stripped too, so restoring into a managed MySQL instance (which usually can't grant the privilege to recreate someone else's `DEFINER`) does not fail.
109
+ exwiw generates INSERT statements only it does not generate DELETE statements. Import into an empty database (`insert-000-schema.sql` provisions one), or clear the target's rows yourself before importing.
110
+
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. 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` and `CREATE TRIGGER` statements are wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object`. For `mysql`, the source server's `DEFINER=user@host` stamp on views and triggers is stripped too, so restoring into a managed MySQL instance (which usually can't grant the privilege to recreate someone else's `DEFINER`) does not fail.
112
+
113
+ The schema file carries the source's triggers. They are suppressed while the target is loaded: each `insert-NNN-<table>.sql` opens with a block that sets `session_replication_role = 'replica'` for the connection, which turns off both user triggers and foreign-key enforcement for the statements that follow (the PostgreSQL counterpart of the `FOREIGN_KEY_CHECKS=0` `mysql` dumps already carry). Setting it requires superuser (`rds_superuser` on RDS); if the restoring role lacks the privilege the block reports a `WARNING` and the load proceeds with triggers firing. The setting applies to the connection and is not reset at the end of each file — every file re-arms it itself, so `psql -f` per file and `cat insert-*.sql | psql` both work, but a session that sources these files and then goes on to do other work stays in replica mode; reset it yourself (`SET session_replication_role = 'origin'`) in that case. `sqlite` has no equivalent and loads with its triggers active.
111
114
 
112
115
  For `postgresql`, the extensions a managed platform installs to run the source instance itself are treated as out of target and left out of the dump entirely — currently `google_vacuum_mgmt` (Cloud SQL / AlloyDB adaptive autovacuum), `google_columnar_engine` and `google_db_advisor` (AlloyDB). They serve the source instance's operation (vacuum tuning, the in-memory columnar cache, index advice), hold no application data, are referenced by nothing in the application's own schema, and ship only with the managed platform, so a restore target outside it can never create them. Their schemas are dropped via `pg_dump --exclude-schema` and their `CREATE EXTENSION` / `COMMENT ON EXTENSION` statements — which are not schema-qualified, so no `pg_dump` filter reaches them — are removed from the output; whatever was excluded is named in the run's log.
113
116
 
@@ -116,11 +119,6 @@ The list is exact names, not a `google_*` prefix match: those prefixes are not r
116
119
  - a third-party extension pulled in as a dependency of an excluded one (`google_db_advisor` requires `hypopg`), since that one *is* installable on a plain PostgreSQL, and
117
120
  - an application-facing platform extension (`google_ml_integration`, `alloydb_scann`, `alloydb_ai_nl`), which the application's own SQL and DDL can name (a ScaNN index is `USING scann`) — removing its `CREATE` would strand whatever refers to it, so it warns and skips instead.
118
121
 
119
- you need to delete the records before importing the dump,
120
- `delete-{idx}-{table_name}.sql` will help you to do that.
121
- This sql will delete "all" related records to the extract targets.
122
- idx meaning is the same as insert sql.
123
-
124
122
  ### `exwiw explain`
125
123
 
126
124
  Print the query each `export` would run together with its `EXPLAIN` output, to stdout. For the SQL adapters (`mysql`, `postgresql`, `sqlite`) this is the compiled SELECT plus its `EXPLAIN` (estimate-only; `EXPLAIN QUERY PLAN` on SQLite) — no SELECT is executed. For `mongodb` it is the `find` description plus the server's explain document as JSON.
@@ -135,7 +133,7 @@ exwiw explain \
135
133
  --target-table=shops --ids=1
136
134
  ```
137
135
 
138
- The `--output-dir`, `--output-format`, `--insert-only`, and `--after-insert-hook` options are dump-specific and rejected when used with `explain`.
136
+ The `--output-dir`, `--output-format`, and `--after-insert-hook` options are dump-specific and rejected when used with `explain`.
139
137
 
140
138
  MongoDB-specific explain behavior — the configurable verbosity (`queryPlanner` / `executionStats` / `allPlansExecution`) and how scoped collections are shown — is described in [MongoDB support](docs/mongodb.md#exwiw-explain-verbosity).
141
139
 
@@ -307,7 +305,6 @@ adapter: postgresql
307
305
  schema_dir: exwiw/schema
308
306
  output_dir: dump
309
307
  output_format: insert # insert | copy
310
- insert_only: false
311
308
  after_insert_hook: hooks/seed.rb
312
309
  log_level: info # debug | info
313
310
  # target_table / ids / ids_field / scope_column may also be set here
@@ -326,8 +323,8 @@ Notes:
326
323
 
327
324
  - **Database connection settings stay on the CLI/environment.** `host`, `port`, `user`, `database`, `uri`, and `password` are **rejected** in the config file (exwiw exits with an error). `adapter` is the one connection-related key that *is* allowed in the file.
328
325
  - **Relative paths in the config (`schema_dir`, `output_dir`, `after_insert_hook`) are resolved relative to the config file's own directory**, not the current working directory. So with the config at the project root, `schema_dir: exwiw/schema` reads naturally, and an absolute `--config=/path/to/exwiw.yml` works no matter where you run from. (CLI path flags remain relative to the current directory — each source resolves relative to where it is written.) Absolute paths are used as-is.
329
- - Unknown keys are rejected so a typo surfaces immediately.
330
- - Export-only keys (`output_dir`, `output_format`, `insert_only`, `after_insert_hook`) are ignored when running `explain` or `schema`, so a single config file can be shared by every subcommand.
326
+ - Unknown keys are rejected so a typo surfaces immediately. (`insert_only`, whose behavior was removed, is the one grandfathered key: accepted and ignored with a warning.)
327
+ - Export-only keys (`output_dir`, `output_format`, `after_insert_hook`) are ignored when running `explain` or `schema`, so a single config file can be shared by every subcommand.
331
328
  - `explain_verbosity` sets the mongodb `explain` verbosity (`queryPlanner` | `executionStats` | `allPlansExecution`, default `queryPlanner`); the `EXWIW_MONGODB_EXPLAIN_VERBOSITY` env var overrides it. Ignored by the SQL adapters and by `export`. See [MongoDB support](docs/mongodb.md#exwiw-explain-verbosity).
332
329
  - `mongodb_query_timeout_ms` sets the global, server-enforced query timeout (mongodb only); the `--mongodb-query-timeout-ms` CLI flag overrides it. Ignored by the SQL adapters. See [MongoDB support](docs/mongodb.md).
333
330
 
@@ -618,13 +615,9 @@ psql -d app_dev -f dump/insert-001-shops.sql
618
615
 
619
616
  `--output-format=copy` is only supported with the `postgresql` adapter.
620
617
 
621
- ### Skip DELETE SQL output
622
-
623
- By default, exwiw generates `delete-*.sql` files alongside the `insert-*.sql` files so that an existing dataset can be cleared before re-inserting. Pass `--insert-only` when you only need the insert files.
624
-
625
618
  ### After-insert hook
626
619
 
627
- `--after-insert-hook=PATH` runs a post-processing hook **after** all per-table insert/delete files have been written. The hook can be either a Ruby file (`.rb`) or any executable script (e.g. `.sh`).
620
+ `--after-insert-hook=PATH` runs a post-processing hook **after** all per-table insert files have been written. The hook can be either a Ruby file (`.rb`) or any executable script (e.g. `.sh`).
628
621
 
629
622
  **Ruby hook (`.rb`)**: provides a tiny DSL with these builtins:
630
623
 
@@ -666,7 +659,7 @@ Note: Ruby hooks are evaluated via `instance_eval` inside the exwiw process —
666
659
 
667
660
  ### Ignore a table
668
661
 
669
- Set `"ignore": true` on a table's config JSON to exclude it from data extraction. The table's DDL is still emitted into `insert-000-schema.{sql,js}` so the schema stays consistent, but no `insert-*` / `delete-*` files are generated for it and the table is never queried.
662
+ Set `"ignore": true` on a table's config JSON to exclude it from data extraction. The table's DDL is still emitted into `insert-000-schema.{sql,js}` so the schema stays consistent, but no `insert-*` files are generated for it and the table is never queried.
670
663
 
671
664
  ```json
672
665
  {
@@ -772,7 +765,7 @@ WHERE reviews.reviewable_id IN (/* products subquery */)
772
765
  AND reviews.reviewable_type = 'Product'
773
766
  ```
774
767
 
775
- The same type filter is applied on the join path — and in the matching `delete-*.sql` bulk-delete subquery — when the polymorphic table is an intermediate hop rather than the directly-dumped table.
768
+ The same type filter is applied on the join path when the polymorphic table is an intermediate hop rather than the directly-dumped table.
776
769
 
777
770
  #### Every arm is extracted (scope-column mode)
778
771
 
@@ -912,7 +905,6 @@ Behavior at dump time:
912
905
 
913
906
  - Extraction uses `SELECT *` so the dump is robust against Rails-side column additions.
914
907
  - `INSERT` statements omit the column list (`INSERT INTO schema_migrations VALUES (...)`). For PostgreSQL `--output-format=copy`, the `COPY` header similarly omits the column list (`COPY schema_migrations FROM stdin;`).
915
- - No `delete-*.sql` file is generated for rails-managed tables, to avoid wiping migration history on the import target.
916
908
 
917
909
  Constraints:
918
910
 
@@ -982,7 +974,6 @@ An explicit id list of that size is exactly estimated and selective, so the fore
982
974
  - The batch table's ids come from **its own extraction query**, so it is narrowed by exactly the filter it would carry in the unbatched query. They are held in memory for the extraction: one scope's worth of primary keys, orders of magnitude smaller than the table being batched.
983
975
  - The batch table may be **any number of hops up** the path — a table two hops below it (`activity_orders → activities → customers`) names `customers` too, and the batch ids are applied where the path meets the scope, bounding the whole join chain.
984
976
  - A table that **carries the scope column itself** batches by naming itself; each batch then filters `WHERE <pk> IN (<ids>)` directly. Note that the id-set query is then the same scope predicate over the same table, so this shape only avoids the scan when the scope column is indexed (ideally index-only) — the join shape above is the one that genuinely removes the planner's choice.
985
- - `delete-*.sql` is unaffected (it is generated from the unbatched query).
986
977
  - `bulk_insert_chunk_size` is independent: batches are query boundaries, chunks are `INSERT` statement boundaries.
987
978
  - With `--output-format=copy`, batching bounds each query's cost but not memory: COPY builds the whole table's body in memory, so all batches' rows are resident at once. Use the default INSERT format (which streams) when the kept rows themselves are huge.
988
979
 
@@ -1186,9 +1177,6 @@ exwiw can export MongoDB databases too (`--adapter=mongodb`): JSONL output impor
1186
1177
  - Generate the full list of INSERT sql based on the specified conditions.
1187
1178
  - If the processing table has no relation with target tables, then dump all records.
1188
1179
  - If the processing table has relation with target tables, then dump the records which are related to the target tables.
1189
- - Generate the full list of DELETE sql based on the specified conditions.
1190
- - If the processing table has no relation with target tables, then delete all records.
1191
- - If the processing table has relation with target tables, then delete the records which are related to the target tables.
1192
1180
 
1193
1181
  ## Development
1194
1182
 
data/docs/mongodb.md CHANGED
@@ -32,7 +32,7 @@ exwiw can export a MongoDB database with `--adapter=mongodb`. This document coll
32
32
  ```bash
33
33
  mongosh "mongodb://localhost/app_dev" dump/insert-000-schema.js
34
34
  ```
35
- - Unlike SQL adapters, the MongoDB adapter does not emit `delete-*.jsonl` files (drop the database / collection yourself before importing if needed).
35
+ - exwiw emits no delete files; drop the database / collection yourself before importing if needed.
36
36
 
37
37
  ## Masking
38
38
 
@@ -170,8 +170,8 @@ module Exwiw
170
170
 
171
171
  # The subset of SQLite keywords (https://sqlite.org/lang_keywords.html)
172
172
  # that actually fail to parse as bare identifiers in the positions exwiw
173
- # emits (qualified column, INSERT column list, FROM/DELETE/JOIN table
174
- # name, CASE masking, derived-table scope JOIN). SQLite's parser accepts
173
+ # emits (qualified column, INSERT column list, FROM/JOIN table name,
174
+ # CASE masking, derived-table scope JOIN). SQLite's parser accepts
175
175
  # the other ~half of its keywords as identifiers via fallback (e.g.
176
176
  # `key`, `temp`, `row`), and those are deliberately NOT quoted so output
177
177
  # for such names stays byte-identical with previous releases — a name in
@@ -217,10 +217,6 @@ module Exwiw
217
217
  end.join("\n")
218
218
  end
219
219
 
220
- def to_bulk_delete(_query, _config)
221
- raise NotImplementedError, "MongodbAdapter does not support bulk delete"
222
- end
223
-
224
220
  # Default explain verbosity. `queryPlanner` asks the server to PLAN the
225
221
  # query without executing it, so it is safe to run against a production
226
222
  # source — no documents are scanned or returned. `executionStats` and
@@ -325,10 +321,6 @@ module Exwiw
325
321
  @logger.info(" Wrote schema for #{collections.size} collection(s) to #{output_path}.")
326
322
  end
327
323
 
328
- def supports_bulk_delete?
329
- false
330
- end
331
-
332
324
  # `--ids` from the CLI arrives as Strings. Mongo compares types strictly,
333
325
  # so the textual ids must be coerced to the type actually stored in `_id`:
334
326
  #
@@ -26,7 +26,7 @@ module Exwiw
26
26
  # so the row count is independent of the projected columns.
27
27
  # - the stream ties up the connection until fully drained. The Runner
28
28
  # always drains it (write_inserts) before any further query
29
- # (post_insert_sql / DELETE), and MysqlClient#stream_rows drains the
29
+ # (post_insert_sql), and MysqlClient#stream_rows drains the
30
30
  # remainder if iteration is abandoned, so the connection stays usable.
31
31
  class StreamingResult
32
32
  include Enumerable
@@ -171,64 +171,6 @@ module Exwiw
171
171
  end
172
172
  end
173
173
 
174
- def to_bulk_delete(select_query_ast, table)
175
- raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
176
-
177
- sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
178
-
179
- if select_query_ast.join_clauses.empty?
180
- # Ignore filter option, because bulk delete is for cleaning before import,
181
- # so it should delete all records to avoid foreign key violation & data consistancy.
182
- compiled_where_conditions = select_query_ast.
183
- where_clauses.
184
- select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
185
- map do |where|
186
- compile_delete_where_condition(where, select_query_ast.from_table_name)
187
- end
188
-
189
- if compiled_where_conditions.size > 0
190
- sql += "\nWHERE "
191
- sql += compiled_where_conditions.join(' AND ')
192
- end
193
- sql += ";"
194
-
195
- return sql
196
- end
197
-
198
- subquery_ast = Exwiw::QueryAst::Select.new
199
- first_join = select_query_ast.join_clauses.first.clone
200
-
201
- subquery_ast.from(first_join.join_table_name)
202
- primay_key_col = table.columns.find { |col| col.name == table.primary_key }
203
- subquery_ast.select([primay_key_col])
204
- select_query_ast.join_clauses[1..].each do |join|
205
- subquery_ast.join(join)
206
- end
207
- first_join.where_clauses.each do |where|
208
- # Ignore filter option, because bulk delete is for cleaning before import,
209
- # so it should delete all records to avoid foreign key violation & data consistancy.
210
- subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
211
- end
212
-
213
- foreign_key = first_join.foreign_key
214
- subquery_sql = compile_ast(subquery_ast)
215
- sql += "\nWHERE #{qualified_name(select_query_ast.from_table_name, foreign_key)} IN (#{subquery_sql})"
216
-
217
- # first_join.base_where_clauses holds conditions on the outer
218
- # delete-target table (from_table_name), such as a polymorphic type
219
- # column. They are not part of the subquery, so add them to the outer
220
- # WHERE. This prevents deleting rows that belong to a different
221
- # polymorphic type.
222
- first_join.base_where_clauses.each do |where|
223
- next unless where.is_a?(Exwiw::QueryAst::WhereClause)
224
-
225
- sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
226
- end
227
- sql += ";"
228
-
229
- sql
230
- end
231
-
232
174
  # @param count_only [Boolean] emit `SELECT COUNT(*)` instead of the
233
175
  # projected columns (used by StreamingResult#size). Safe because exwiw's
234
176
  # extraction queries have no DISTINCT/GROUP BY/LIMIT, so the count does
@@ -377,39 +319,6 @@ module Exwiw
377
319
  name
378
320
  end
379
321
 
380
- # A WHERE condition for the DELETE statement.
381
- #
382
- # MySQL refuses a subquery that reads the table being deleted from
383
- # ("You can't specify target table 'x' for update in FROM clause"), and a
384
- # polymorphic multi-arm scope produces exactly that: each arm selects the
385
- # join table's own primary key, so the delete's `pk IN (…)` reads the
386
- # delete target. Wrapping the subquery in a derived table lifts the
387
- # restriction — MySQL materializes the derived table before the DELETE
388
- # runs, so the rows deleted are the ones the SELECT matched.
389
- #
390
- # Only that self-referencing shape is wrapped; every other subquery
391
- # (a scope id-set projected from *another* table, the ids_field probe)
392
- # compiles exactly as before.
393
- private def compile_delete_where_condition(where_clause, table_name)
394
- if where_clause.operator == :in_subquery && delete_target_self_reference?(where_clause.value, table_name)
395
- key = qualified_name(table_name, where_clause.column_name)
396
- return "#{key} IN (SELECT * FROM (#{compile_subquery(where_clause.value)}) AS exwiw_delete_src)"
397
- end
398
-
399
- compile_where_condition(where_clause, table_name)
400
- end
401
-
402
- private def delete_target_self_reference?(subquery, table_name)
403
- case subquery
404
- when Exwiw::QueryAst::SelectSubquery
405
- Exwiw::QueryAst.reads_table?(subquery.query, table_name)
406
- when Exwiw::QueryAst::UnionSubquery
407
- subquery.queries.any? { |query| Exwiw::QueryAst.reads_table?(query, table_name) }
408
- else
409
- false
410
- end
411
- end
412
-
413
322
  private def compile_where_condition(where_clause, table_name)
414
323
  # Use as it is if it's a raw query
415
324
  return where_clause if where_clause.is_a?(String)
@@ -24,8 +24,8 @@ module Exwiw
24
24
  # unchanged, so MongoDB and the other SQL adapters are untouched.
25
25
  # - the streaming pass ties up the connection until fully drained. The
26
26
  # Runner always drains it (write_inserts) before issuing any further
27
- # query (post_insert_sql / DELETE) on the same connection, so the
28
- # ordering invariant holds.
27
+ # query (post_insert_sql) on the same connection, so the ordering
28
+ # invariant holds.
29
29
  class StreamingResult
30
30
  include Enumerable
31
31
 
@@ -186,6 +186,13 @@ module Exwiw
186
186
  # EXTENSION that pg_dump emits alongside is likewise wrapped to swallow
187
187
  # undefined_object, so a skipped extension's trailing comment does not
188
188
  # abort the restore either.
189
+ # Triggers are wrapped the same way. They used to be stripped instead,
190
+ # because a `--table` dump emitted CREATE TRIGGER without the
191
+ # CREATE FUNCTION it referenced; a whole-database dump carries both, so a
192
+ # target can have the source's triggers instead of silently running none
193
+ # (mysqldump and sqlite_master always emitted theirs). They do fire while
194
+ # the `insert-*.sql` files are applied, so a load that must not fire them
195
+ # disables them for the session, as a mysql restore already had to.
189
196
  # Platform-managed extensions are stripped first: the wrapping passes below
190
197
  # rewrite the bare CREATE/COMMENT statements this removes.
191
198
  idempotent = strip_platform_managed_extensions(stdout)
@@ -197,7 +204,7 @@ module Exwiw
197
204
  idempotent = DdlPostprocessor.add_if_not_exists_to_create_table(idempotent)
198
205
  idempotent = DdlPostprocessor.add_if_not_exists_to_create_index(idempotent)
199
206
  idempotent = DdlPostprocessor.wrap_add_constraint_in_do_block(idempotent)
200
- idempotent = DdlPostprocessor.strip_triggers(idempotent)
207
+ idempotent = DdlPostprocessor.wrap_create_trigger_in_do_block(idempotent)
201
208
 
202
209
  File.open(output_path, 'w') do |file|
203
210
  file.puts("-- Auto-generated by exwiw via pg_dump. Idempotent DDL for postgresql.")
@@ -266,6 +273,44 @@ module Exwiw
266
273
  lines.join("\n")
267
274
  end
268
275
 
276
+ def pre_insert_sql(_table)
277
+ suppress_triggers_sql
278
+ end
279
+
280
+ # Suppress trigger and foreign-key enforcement for the statements that
281
+ # follow. insert-000-schema.sql carries the source's triggers now, so an
282
+ # unguarded pass would fire every one of them per row it touches — on the
283
+ # insert side over rows that already carry the values those triggers
284
+ # produced on the source, and in an order that is only guaranteed to
285
+ # satisfy FKs once every insert-*.sql has been applied. `replica` mode
286
+ # turns off both user and system (RI) triggers, matching what MysqlAdapter
287
+ # does with FOREIGN_KEY_CHECKS.
288
+ #
289
+ # Setting the parameter needs superuser (or a grant on it), which a
290
+ # restore role may not have; the pass itself is still valid without it,
291
+ # so a failure is downgraded to a WARNING rather than aborting the file.
292
+ #
293
+ # Unlike MysqlAdapter — and unlike `pg_dump --disable-triggers`, which
294
+ # pairs each table's DISABLE TRIGGER ALL with an ENABLE — no counterpart
295
+ # reset is emitted, so the file is NOT self-contained in session state:
296
+ # sourcing these files into a session that goes on to do other work
297
+ # leaves that session in replica mode. `is_local = false` still bounds it
298
+ # to the connection, and every file re-arms the setting itself, so
299
+ # concatenating the files works either way — the reset would only buy
300
+ # tidiness, at the cost of emitting it unconditionally (post_insert_sql
301
+ # returns nil for a table with no serial PK) inside a second exception
302
+ # handler (a bare reset re-raises insufficient_privilege on the
303
+ # unprivileged path and would abort the file).
304
+ private def suppress_triggers_sql
305
+ <<~SQL.chomp
306
+ DO $exwiw$ BEGIN
307
+ PERFORM set_config('session_replication_role', 'replica', false);
308
+ EXCEPTION WHEN insufficient_privilege THEN
309
+ RAISE WARNING 'exwiw: could not disable triggers for the load (%): %', SQLSTATE, SQLERRM;
310
+ END $exwiw$;
311
+ SQL
312
+ end
313
+
269
314
  # Transcribe the FROM-side sequence cursor backing `table.primary_key`
270
315
  # onto the import target. Without this, importing into a clean DB leaves
271
316
  # the sequence at 1 while the inserted rows occupy higher IDs, so the
@@ -302,73 +347,6 @@ module Exwiw
302
347
  "SELECT pg_catalog.setval('#{escape_single_quote(seq_name)}', #{last_value}, #{is_called_sql});"
303
348
  end
304
349
 
305
- def to_bulk_delete(select_query_ast, table)
306
- raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
307
-
308
- sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
309
-
310
- if select_query_ast.join_clauses.empty?
311
- # Ignore filter option, because bulk delete is for cleaning before import,
312
- # so it should delete all records to avoid foreign key violation & data consistancy.
313
- compiled_where_conditions = select_query_ast.
314
- where_clauses.
315
- select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
316
- map do |where|
317
- compile_where_condition(where, select_query_ast.from_table_name)
318
- end
319
-
320
- if compiled_where_conditions.size > 0
321
- sql += "\nWHERE "
322
- sql += compiled_where_conditions.join(' AND ')
323
- end
324
- sql += ";"
325
-
326
- return sql
327
- end
328
-
329
- subquery_ast = Exwiw::QueryAst::Select.new
330
- first_join = select_query_ast.join_clauses.first.clone
331
-
332
- subquery_ast.from(first_join.join_table_name)
333
- primay_key_col = table.columns.find { |col| col.name == table.primary_key }
334
- subquery_ast.select([primay_key_col])
335
- select_query_ast.join_clauses[1..].each do |join|
336
- subquery_ast.join(join)
337
- end
338
- first_join.where_clauses.each do |where|
339
- # Ignore filter option, because bulk delete is for cleaning before import,
340
- # so it should delete all records to avoid foreign key violation & data consistancy.
341
- subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
342
- end
343
-
344
- foreign_key = first_join.foreign_key
345
- outer_table = select_query_ast.from_table_name
346
- inner_table = first_join.join_table_name
347
- inner_column = first_join.primary_key
348
- cast_to = types_need_cast?(
349
- column_pg_type(outer_table, foreign_key),
350
- column_pg_type(inner_table, inner_column)
351
- ) ? 'text' : nil
352
- subquery_sql = compile_ast(subquery_ast, select_cast_to: cast_to)
353
- outer_expr = qualified_name(outer_table, foreign_key)
354
- outer_expr = "#{outer_expr}::text" if cast_to
355
- sql += "\nWHERE #{outer_expr} IN (#{subquery_sql})"
356
-
357
- # first_join.base_where_clauses holds conditions on the outer
358
- # delete-target table (from_table_name), such as a polymorphic type
359
- # column. They are not part of the subquery, so add them to the outer
360
- # WHERE. This prevents deleting rows that belong to a different
361
- # polymorphic type.
362
- first_join.base_where_clauses.each do |where|
363
- next unless where.is_a?(Exwiw::QueryAst::WhereClause)
364
-
365
- sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
366
- end
367
- sql += ";"
368
-
369
- sql
370
- end
371
-
372
350
  def compile_ast(query_ast, select_cast_to: nil)
373
351
  raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select)
374
352
 
@@ -137,64 +137,6 @@ module Exwiw
137
137
  end
138
138
  end
139
139
 
140
- def to_bulk_delete(select_query_ast, table)
141
- raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
142
-
143
- sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
144
-
145
- if select_query_ast.join_clauses.empty?
146
- # Ignore filter option, because bulk delete is for cleaning before import,
147
- # so it should delete all records to avoid foreign key violation & data consistancy.
148
- compiled_where_conditions = select_query_ast.
149
- where_clauses.
150
- select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
151
- map do |where|
152
- compile_where_condition(where, select_query_ast.from_table_name)
153
- end
154
-
155
- if compiled_where_conditions.size > 0
156
- sql += "\nWHERE "
157
- sql += compiled_where_conditions.join(' AND ')
158
- end
159
- sql += ";"
160
-
161
- return sql
162
- end
163
-
164
- subquery_ast = Exwiw::QueryAst::Select.new
165
- first_join = select_query_ast.join_clauses.first.clone
166
-
167
- subquery_ast.from(first_join.join_table_name)
168
- primay_key_col = table.columns.find { |col| col.name == table.primary_key }
169
- subquery_ast.select([primay_key_col])
170
- select_query_ast.join_clauses[1..].each do |join|
171
- subquery_ast.join(join)
172
- end
173
- first_join.where_clauses.each do |where|
174
- # Ignore filter option, because bulk delete is for cleaning before import,
175
- # so it should delete all records to avoid foreign key violation & data consistancy.
176
- subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
177
- end
178
-
179
- foreign_key = first_join.foreign_key
180
- subquery_sql = compile_ast(subquery_ast)
181
- sql += "\nWHERE #{qualified_name(select_query_ast.from_table_name, foreign_key)} IN (#{subquery_sql})"
182
-
183
- # first_join.base_where_clauses holds conditions on the outer
184
- # delete-target table (from_table_name), such as a polymorphic type
185
- # column. They are not part of the subquery, so add them to the outer
186
- # WHERE. This prevents deleting rows that belong to a different
187
- # polymorphic type.
188
- first_join.base_where_clauses.each do |where|
189
- next unless where.is_a?(Exwiw::QueryAst::WhereClause)
190
-
191
- sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
192
- end
193
- sql += ";"
194
-
195
- sql
196
- end
197
-
198
140
  # @param count_only [Boolean] emit `SELECT COUNT(*)` instead of the
199
141
  # projected columns (used by StreamingResult#size). Safe because exwiw's
200
142
  # extraction queries have no DISTINCT/GROUP BY/LIMIT, so the count does
data/lib/exwiw/adapter.rb CHANGED
@@ -82,11 +82,6 @@ module Exwiw
82
82
  def dump_schema(ordered_tables, output_path)
83
83
  end
84
84
 
85
- # Whether this adapter emits delete-NNN-*.sql files.
86
- def supports_bulk_delete?
87
- true
88
- end
89
-
90
85
  # Whether the given config produces its own dump output and needs an
91
86
  # independent processing pass. SQL adapters always do; non-SQL adapters
92
87
  # may exclude e.g. embedded subdocument configs.
@@ -339,12 +334,6 @@ module Exwiw
339
334
  raise NotImplementedError
340
335
  end
341
336
 
342
- # @params [Exwiw::QueryAst] select_query_ast
343
- # @params [Exwiw::TableConfig] table
344
- def to_bulk_delete(select_query_ast, table)
345
- raise NotImplementedError
346
- end
347
-
348
337
  def self.build(connection_config, logger)
349
338
  case normalize_name(connection_config.adapter)
350
339
  when 'sqlite'
data/lib/exwiw/cli.rb CHANGED
@@ -39,7 +39,6 @@ module Exwiw
39
39
  schema_dir
40
40
  output_dir
41
41
  output_format
42
- insert_only
43
42
  after_insert_hook
44
43
  log_level
45
44
  target_table
@@ -58,6 +57,11 @@ module Exwiw
58
57
  EXPLAIN_VERBOSITIES = %w[queryPlanner executionStats allPlansExecution].freeze
59
58
  DEFAULT_EXPLAIN_VERBOSITY = "queryPlanner"
60
59
 
60
+ # Keys that configured behavior that no longer exists (insert_only toggled
61
+ # the removed delete-*.sql generation). Accepted so a committed config file
62
+ # keeps working across the upgrade, warned about so it gets cleaned up.
63
+ OBSOLETE_CONFIG_KEYS = %w[insert_only].freeze
64
+
61
65
  # Database connection settings are environment-specific (and sometimes
62
66
  # secret-adjacent), so they must be passed via CLI/env, never the committed
63
67
  # config file. `adapter` is the one connection-ish key allowed in config.
@@ -66,7 +70,7 @@ module Exwiw
66
70
  # Keys that only make sense for `export`. They are skipped when merging config
67
71
  # for `explain` so a shared config file does not trip validate_explain_only!,
68
72
  # and for `schema`, which performs no export at all.
69
- EXPORT_ONLY_CONFIG_KEYS = %w[output_dir output_format insert_only after_insert_hook parallel_workers].freeze
73
+ EXPORT_ONLY_CONFIG_KEYS = %w[output_dir output_format after_insert_hook parallel_workers].freeze
70
74
 
71
75
  def self.start(argv)
72
76
  new(argv).run
@@ -111,7 +115,6 @@ module Exwiw
111
115
  @ids_field = nil
112
116
  @scope_column = nil
113
117
  @output_format = nil
114
- @insert_only = nil
115
118
  @after_insert_hook_path = nil
116
119
  @parallel_workers = nil
117
120
  @mongodb_query_timeout_ms = nil
@@ -160,7 +163,6 @@ module Exwiw
160
163
  schema_dir: @schema_dir,
161
164
  dump_target: dump_target,
162
165
  output_format: @output_format,
163
- insert_only: @insert_only,
164
166
  after_insert_hook_path: @after_insert_hook_path,
165
167
  parallel_workers: @parallel_workers,
166
168
  cli_options: build_cli_options_hash,
@@ -333,7 +335,6 @@ module Exwiw
333
335
  if @subcommand == "export"
334
336
  @output_dir ||= "dump"
335
337
  @output_format ||= "insert"
336
- @insert_only = @insert_only ? true : false
337
338
 
338
339
  valid_output_formats = ["insert", "copy"]
339
340
  unless valid_output_formats.include?(@output_format)
@@ -426,6 +427,10 @@ module Exwiw
426
427
  $stderr.puts "'#{key}' is a database connection setting and must be passed via the CLI/environment, not the config file (#{path})"
427
428
  exit 1
428
429
  end
430
+ if OBSOLETE_CONFIG_KEYS.include?(key)
431
+ $stderr.puts "warning: config key '#{key}' in #{path} is obsolete and ignored (exwiw no longer generates delete-*.sql files); remove it from the config file"
432
+ next
433
+ end
429
434
  unless ALLOWED_CONFIG_KEYS.include?(key)
430
435
  $stderr.puts "Unknown config key '#{key}' in #{path}. Allowed keys: #{ALLOWED_CONFIG_KEYS.join(', ')}"
431
436
  exit 1
@@ -443,7 +448,6 @@ module Exwiw
443
448
  @output_dir ||= expand_dir(config["output_dir"], base)
444
449
  @after_insert_hook_path ||= (File.expand_path(config["after_insert_hook"], base) if config["after_insert_hook"])
445
450
  @output_format ||= config["output_format"]
446
- @insert_only = config["insert_only"] if @insert_only.nil? && config.key?("insert_only")
447
451
  @log_level ||= config["log_level"]&.to_sym
448
452
  @target_table_name ||= config["target_table"]
449
453
  @target_collection_name ||= config["target_collection"]
@@ -679,7 +683,6 @@ module Exwiw
679
683
  rejected = []
680
684
  rejected << "--output-dir" unless @output_dir.nil?
681
685
  rejected << "--output-format" unless @output_format.nil?
682
- rejected << "--insert-only" unless @insert_only.nil?
683
686
  rejected << "--after-insert-hook" unless @after_insert_hook_path.nil?
684
687
  rejected << "--parallel-workers" unless @parallel_workers.nil?
685
688
 
@@ -744,7 +747,6 @@ module Exwiw
744
747
  ids_field: @ids_field,
745
748
  scope_column: @scope_column,
746
749
  output_format: @output_format,
747
- insert_only: @insert_only,
748
750
  log_level: @log_level,
749
751
  after_insert_hook: @after_insert_hook_path,
750
752
  }.freeze
@@ -816,8 +818,10 @@ module Exwiw
816
818
  opts.on("--ids-field=[FIELD]", "Field on the target collection that --ids is matched against. Defaults to the primary key. (mongodb adapter only)") { |v| @ids_field = v }
817
819
  opts.on("--scope-column=[COLUMN]", "DEPRECATED. Filter every table by this shared global column (--ids are its values) instead of a single --target-table. SQL adapters only; mutually exclusive with --target-table. Prefer declaring a per-table `scope_column:` in the schema config and running with --target-table.") { |v| @scope_column = v }
818
820
  opts.on("--output-format=[FORMAT]", "Output format: insert (default) or copy (PostgreSQL only, export subcommand only)") { |v| @output_format = v }
819
- opts.on("--insert-only", "Do not generate DELETE SQL files (export subcommand only)") { @insert_only = true }
820
- opts.on("--after-insert-hook=PATH", "Path to a .rb or .sh post-processing hook executed after all insert/delete files are written (export subcommand only)") do |v|
821
+ opts.on("--insert-only", "DEPRECATED: ignored. exwiw no longer generates delete-*.sql files, so every export is insert-only.") do
822
+ $stderr.puts "warning: --insert-only is obsolete and ignored (exwiw no longer generates delete-*.sql files); remove the flag"
823
+ end
824
+ opts.on("--after-insert-hook=PATH", "Path to a .rb or .sh post-processing hook executed after all insert files are written (export subcommand only)") do |v|
821
825
  @after_insert_hook_path = File.expand_path(v)
822
826
  end
823
827
  opts.on("--parallel-workers=N", Integer, "Fork N workers for the MongoDB dump's parallel schedule (mongodb + export only; N>=2 enables it, default is serial). Output is byte-identical to serial; falls back to serial where fork is unavailable.") { |v| @parallel_workers = v }
@@ -58,10 +58,50 @@ module Exwiw
58
58
  end
59
59
  end
60
60
 
61
- # pg_dump --table includes triggers but not the referenced function
62
- # definitions, causing UndefinedFunction errors on the target DB.
63
- def strip_triggers(sql)
64
- sql.gsub(/^[ \t]*CREATE\s+(?:OR\s+REPLACE\s+)?(?:CONSTRAINT\s+)?TRIGGER\b[^;]*;\r?\n?/i, "")
61
+ # One trigger as pg_dump writes it: its header comment block ($1) followed by
62
+ # the `CREATE TRIGGER` statement ($2).
63
+ #
64
+ # --
65
+ # -- Name: users set_timestamp; Type: TRIGGER; Schema: public; Owner: -
66
+ # --
67
+ #
68
+ # CREATE TRIGGER set_timestamp BEFORE UPDATE ON public.users ...;
69
+ #
70
+ # The header is what keeps the pass off a `CREATE TRIGGER` line inside a
71
+ # dollar-quoted function body — a function that installs a trigger itself,
72
+ # whose meaning a rewrite would change. pg_dump never writes a header block
73
+ # inside a body, so requiring one limits the match to top-level statements.
74
+ # (The stripping pass this replaced matched the bare form and deleted such a
75
+ # line, leaving a function that compiled but installed nothing.)
76
+ #
77
+ # The statement ends at the first semicolon outside a single-quoted string, so
78
+ # a `;` in a WHEN clause or an argument (`EXECUTE FUNCTION f('a;b')`) does not
79
+ # truncate it. A trigger definition has no body, so no dollar quoting either.
80
+ CREATE_TRIGGER_RE = /
81
+ (^--\r?\n
82
+ --[ \t]+Name:[^\n]*;[ \t]*Type:[ \t]*TRIGGER;[^\n]*\r?\n
83
+ --\r?\n
84
+ (?:[ \t]*\r?\n)*)
85
+ ([ \t]*CREATE\s+(?:OR\s+REPLACE\s+)?(?:CONSTRAINT\s+)?TRIGGER\b
86
+ (?:[^;']|'(?:[^']|'')*')*;)
87
+ /xi.freeze
88
+
89
+ # A bare `CREATE TRIGGER ...;` is not idempotent (a second restore raises
90
+ # `duplicate_object`, 42710), so each statement goes into a DO block that
91
+ # swallows that error, in the form #wrap_add_constraint_in_do_block uses; the
92
+ # header is left as written. `CREATE OR REPLACE TRIGGER` would be shorter but
93
+ # is PostgreSQL 14+ only and pg_dump never emits it.
94
+ def wrap_create_trigger_in_do_block(sql)
95
+ sql.gsub(CREATE_TRIGGER_RE) do
96
+ header = Regexp.last_match(1)
97
+ stmt = Regexp.last_match(2).strip
98
+ <<~SQL.chomp
99
+ #{header}DO $exwiw$ BEGIN
100
+ #{stmt}
101
+ EXCEPTION WHEN duplicate_object THEN NULL;
102
+ END $exwiw$;
103
+ SQL
104
+ end
65
105
  end
66
106
 
67
107
  # A user@host pair as mysqldump writes it. Each side is independently a
data/lib/exwiw/runner.rb CHANGED
@@ -11,7 +11,6 @@ module Exwiw
11
11
  dump_target:,
12
12
  logger:,
13
13
  output_format: 'insert',
14
- insert_only: false,
15
14
  after_insert_hook_path: nil,
16
15
  parallel_workers: nil,
17
16
  cli_options: {}
@@ -21,7 +20,6 @@ module Exwiw
21
20
  @schema_dir = schema_dir
22
21
  @dump_target = dump_target
23
22
  @output_format = output_format
24
- @insert_only = insert_only
25
23
  @after_insert_hook_path = after_insert_hook_path
26
24
  @parallel_workers = parallel_workers
27
25
  @cli_options = cli_options
@@ -105,8 +103,8 @@ module Exwiw
105
103
 
106
104
  # `batch_scope` splits the extraction into one query per slice of the
107
105
  # scope's id set, streaming rows like any adapter result. `query_ast`
108
- # stays the unbatched query — the DELETE file and the error message
109
- # below describe that one.
106
+ # stays the unbatched query — the error message below describes that
107
+ # one.
110
108
  phase = "resolving the batch_scope id set"
111
109
  batched = BatchedExtraction.build(
112
110
  adapter: adapter,
@@ -184,21 +182,6 @@ module Exwiw
184
182
 
185
183
  @logger.info(" Generated INSERT statement for #{record_num} records (#{statement_count} statement(s)).")
186
184
  end
187
-
188
- if adapter.supports_bulk_delete? && !@insert_only && !(table.respond_to?(:rails_managed?) && table.rails_managed?)
189
- phase = "generating DELETE statement"
190
- @logger.debug(" Generate DELETE statement...")
191
- delete_sql = adapter.to_bulk_delete(query_ast, table)
192
- if @logger.debug?
193
- @logger.debug(" Generated DELETE statement:\n#{delete_sql}")
194
- else
195
- @logger.info(" Generated DELETE statement.")
196
- end
197
- delete_idx = (total_size - idx).to_s.rjust(3, '0')
198
- File.open(File.join(@output_dir, "delete-#{delete_idx}-#{table_name}.#{adapter.output_extension}"), 'w') do |file|
199
- file.puts(delete_sql)
200
- end
201
- end
202
185
  rescue => e
203
186
  @logger.error("Error while #{phase} for table '#{table_name}' (#{idx + 1}/#{total_size}): #{e.class}: #{e.message}")
204
187
  @logger.error(" Extraction query that produced the data being processed:")
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.9.24"
4
+ VERSION = "1.0.0"
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.9.24
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia