exwiw 0.9.10 → 0.9.11
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/exwiw/adapter/identifier_quoting.rb +233 -0
- data/lib/exwiw/adapter/mysql_adapter.rb +21 -19
- data/lib/exwiw/adapter/postgresql_adapter.rb +30 -22
- data/lib/exwiw/adapter/sqlite_adapter.rb +18 -16
- data/lib/exwiw/adapter.rb +9 -2
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f85d2a708bc4da44d7a32b0ef91dcf42967b65f5ddcfca516e0ce674de568b4
|
|
4
|
+
data.tar.gz: 0eb9ea0cd15abd41d64380170323070c0961a37a1bd796bfd9c1c63484c8b6d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 645793841437d64ad976c84e89dc7f0bde405dbc549a953b870085a6fac26886060535a69bc3f41f5480f4dc31d5c15ce97b598d0146bb96981004cdcee3679e
|
|
7
|
+
data.tar.gz: 15efb4638a90daf07b9cc83cf2f6613fd0ecee93daaff6fbad40608002d0bf26c4bf61a64a83389d7e672101a6d15c3b4250e34b7daa202d176c62d931b1a7a5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.11] - 2026-07-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Reserved-word and special-character identifiers are now quoted (SQL adapters).** Table and column names that are reserved words (`order`, `from`, `group`, …) or contain characters invalid as a bare identifier are conditionally quoted — backticks on mysql, double quotes on postgresql/sqlite — across every emission path: SELECT projection and masking expressions, FROM/JOIN, WHERE, subqueries, materialized scope JOINs, INSERT / COPY headers and DELETE. Previously only the mysql INSERT header was quoted ([#83](https://github.com/heyinc/exwiw/pull/83)), so a reserved table name broke the extraction SELECT everywhere, a reserved column broke postgresql/sqlite INSERTs, and sqlite rejected even table-qualified reserved columns. Ordinary names stay bare, so output for existing configs is byte-identical (mysql INSERT headers keep their always-backtick form from 0.4.5). Dotted table names are treated as schema qualification and quoted per part (`billing.order` → `` billing.`order` ``), preserving Rails multi-schema `table_name`s. The reserved-word lists cover MySQL 8.4 and 9.x (including 9's `LIBRARY`) plus MariaDB-specific words, PostgreSQL's fully reserved key words, and — for sqlite — exactly the keywords that fail to parse bare in exwiw's emission positions, so fallback-accepted names like `key` are not churned. The lists are enforced by specs that probe the live mysql/postgresql servers' own keyword catalogs and re-derive the sqlite set empirically.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **PostgreSQL: post-import sequence sync now works for reserved or mixed-case table names.** `post_insert_sql` passed the raw table name to `pg_get_serial_sequence`, which parses its first argument with identifier rules and case-folds it — so a table like `"Order"` (now extractable thanks to identifier quoting) failed with `relation "order" does not exist` right after a successful data pass. The conditionally quoted name is passed instead; ordinary lowercase names are unaffected.
|
|
14
|
+
|
|
5
15
|
## [0.9.10] - 2026-07-09
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -23,6 +33,7 @@
|
|
|
23
33
|
### Changed
|
|
24
34
|
|
|
25
35
|
- **Unknown keys in schema config JSON are now rejected on load instead of being silently dropped.** Serdes deserialization is lenient, so a key that matched no declared attribute was discarded without a word — turning a typo (`reverse_scop`, `bulk_insert_chunk_sise`) or a key another adapter supports but this one does not (`raw_sql`/`map` on a MongoDB field; `reverse_scope` on a MongoDB collection before this release) into a silent no-op: the dump ran and the requested masking/scoping simply never happened. `TableConfig.from` / `MongodbCollectionConfig.from` now validate the raw hash against the declared attributes — including the nested `belongs_tos` / `columns` / `fields` / `reverse_scope` / `embedded_in` / `replace_with_fake_data` entries — and raise `Exwiw::UnknownConfigKeyError` (an `ArgumentError` subclass) naming the key(s), the table/collection, the nested position, and the allowed keys; `export`/`explain` prepend the offending file path. This is a deliberate hard error with no opt-out: every declared key still passes — including the documentation-only `comment` on table/collection configs and their `belongs_tos`/`columns`/`fields` entries, which remains the supported place for free-form notes — so a config that only uses supported keys is unaffected, while anything now rejected was already being ignored. If a config carries stray keys, remove them or fold them into `comment`.
|
|
36
|
+
>>>>>>> 71a0958eea6e031d2beeb7349bea35cb460414ca
|
|
26
37
|
|
|
27
38
|
## [0.9.7] - 2026-07-08
|
|
28
39
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Exwiw
|
|
6
|
+
module Adapter
|
|
7
|
+
# Conditional identifier quoting shared by the SQL adapters (mysql /
|
|
8
|
+
# postgresql / sqlite).
|
|
9
|
+
#
|
|
10
|
+
# Table and column names are emitted bare unless they NEED quoting — a
|
|
11
|
+
# reserved word for that database, or characters outside the safe
|
|
12
|
+
# bare-identifier set (e.g. a column named `from`, which is a syntax error
|
|
13
|
+
# unquoted in every dialect's INSERT column list and in sqlite even when
|
|
14
|
+
# table-qualified). Quoting conditionally rather than always (Rails-style)
|
|
15
|
+
# keeps the generated SQL byte-identical to previous exwiw releases for
|
|
16
|
+
# ordinary names, so existing dump snapshots and diffs stay stable.
|
|
17
|
+
# Known exception to that byte-identity promise: mysql-only exotic names —
|
|
18
|
+
# digit-leading (`2fa_codes`) — are valid bare in mysql but fall outside
|
|
19
|
+
# BARE_IDENTIFIER_PATTERN and are now backtick-quoted (still valid SQL,
|
|
20
|
+
# cosmetic difference only).
|
|
21
|
+
#
|
|
22
|
+
# Table names get dot-aware handling (#quote_table_name): a Rails
|
|
23
|
+
# multi-schema app sets `self.table_name = "billing.invoices"` and the
|
|
24
|
+
# schema generator copies it verbatim into the config, so the name must be
|
|
25
|
+
# emitted as two identifiers (`billing."order"`), never quoted whole
|
|
26
|
+
# (`"billing.invoices"` names a nonexistent relation). An identifier with
|
|
27
|
+
# a literal dot in it cannot be expressed — but its bare form was already
|
|
28
|
+
# misparsed as schema.table before quoting existed, so nothing regresses.
|
|
29
|
+
#
|
|
30
|
+
# Each adapter includes its dialect submodule (Mysql / Postgresql /
|
|
31
|
+
# Sqlite below), which bundles the quote character with the matching
|
|
32
|
+
# reserved-word set. `Base` includes the bare module so shared helpers
|
|
33
|
+
# (Base#null_preserving) can rely on #qualified_name existing; the hooks
|
|
34
|
+
# raise NotImplementedError until a dialect submodule provides them.
|
|
35
|
+
#
|
|
36
|
+
# The word sets may over-approximate what strictly needs quoting in a
|
|
37
|
+
# given position (e.g. mysql accepts a reserved word unquoted right after
|
|
38
|
+
# a `.`): quoting a word that did not need it is still valid SQL and — for
|
|
39
|
+
# the lowercase names these lists contain — refers to the same identifier,
|
|
40
|
+
# while missing one is a syntax error. Reservedness is checked
|
|
41
|
+
# case-insensitively but the identifier is quoted with its exact spelling
|
|
42
|
+
# preserved, matching the catalog-exact names exwiw configs carry.
|
|
43
|
+
#
|
|
44
|
+
# spec/adapter/identifier_quoting_spec.rb keeps the lists honest: it
|
|
45
|
+
# probes every sqlite keyword in every emission context and asserts the
|
|
46
|
+
# live mysql / postgresql servers' own reserved-word catalogs are covered.
|
|
47
|
+
module IdentifierQuoting
|
|
48
|
+
# A name every supported dialect accepts bare: leading letter or
|
|
49
|
+
# underscore, then letters, digits, `_` or `$` (`$` verified bare-valid
|
|
50
|
+
# on mysql, postgresql and sqlite — and postgresql resolves such names
|
|
51
|
+
# by case-folding, so quoting them would break configs that spell a
|
|
52
|
+
# folded name in mixed case).
|
|
53
|
+
BARE_IDENTIFIER_PATTERN = /\A[A-Za-z_][A-Za-z0-9_$]*\z/
|
|
54
|
+
|
|
55
|
+
# Quote a single identifier (column name, or one part of a table name)
|
|
56
|
+
# only when the bare form would not parse (reserved word or unsafe
|
|
57
|
+
# characters).
|
|
58
|
+
def quote_identifier(name)
|
|
59
|
+
name = name.to_s
|
|
60
|
+
if name.match?(BARE_IDENTIFIER_PATTERN) && !reserved_words.include?(name.downcase)
|
|
61
|
+
name
|
|
62
|
+
else
|
|
63
|
+
force_quote_identifier(name)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Always-quoted form of a single identifier (embedded quote chars
|
|
68
|
+
# doubled). Used where the adapter's output format has historically
|
|
69
|
+
# always quoted (mysql INSERT headers) and must stay byte-identical.
|
|
70
|
+
def force_quote_identifier(name)
|
|
71
|
+
quote_char = identifier_quote_char
|
|
72
|
+
"#{quote_char}#{name.to_s.gsub(quote_char) { quote_char * 2 }}#{quote_char}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Table-name form: dot-aware. `billing.invoices` is a schema-qualified
|
|
76
|
+
# name (two identifiers), so each dot-separated part is quoted
|
|
77
|
+
# independently — `billing."order"`, not `"billing.order"`.
|
|
78
|
+
def quote_table_name(name)
|
|
79
|
+
name.to_s.split(".", -1).map { |part| quote_identifier(part) }.join(".")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Always-quoted, dot-aware table name (mysql INSERT headers). Splitting
|
|
83
|
+
# changes PR #83's output only for dotted names, whose whole-name quoting
|
|
84
|
+
# (`` `billing.invoices` ``) named a nonexistent table anyway.
|
|
85
|
+
def force_quote_table_name(name)
|
|
86
|
+
name.to_s.split(".", -1).map { |part| force_quote_identifier(part) }.join(".")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `<table>.<column>` with the table part dot-aware and every identifier
|
|
90
|
+
# quoted as needed.
|
|
91
|
+
def qualified_name(table_name, column_name)
|
|
92
|
+
"#{quote_table_name(table_name)}.#{quote_identifier(column_name)}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def identifier_quote_char
|
|
96
|
+
raise NotImplementedError, "#{self.class} must provide #identifier_quote_char (include an IdentifierQuoting dialect submodule)"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private def reserved_words
|
|
100
|
+
raise NotImplementedError, "#{self.class} must provide #reserved_words (include an IdentifierQuoting dialect submodule)"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# MySQL reserved words: the 8.4 manual's "(R)" entries
|
|
104
|
+
# (https://dev.mysql.com/doc/refman/8.4/en/keywords.html), plus the
|
|
105
|
+
# 9.x additions (verified against a live 9.7 server's
|
|
106
|
+
# INFORMATION_SCHEMA.KEYWORDS by the identifier_quoting spec), plus the
|
|
107
|
+
# MariaDB-specific reserved words (https://mariadb.com/kb/en/reserved-words/
|
|
108
|
+
# — exwiw supports MariaDB servers, see MysqlAdapter#mariadb_mysqldump?).
|
|
109
|
+
# Words reserved in only one engine are still safe to quote in the
|
|
110
|
+
# other: backticks are always valid, and names in either engine's list
|
|
111
|
+
# were a syntax error there before, so no previously working output
|
|
112
|
+
# changes on the engine that reserves them.
|
|
113
|
+
MYSQL_RESERVED_WORDS = Set.new(%w[
|
|
114
|
+
accessible add all alter analyze and as asc asensitive before between
|
|
115
|
+
bigint binary blob both by call cascade case change char character
|
|
116
|
+
check collate column condition constraint continue convert create
|
|
117
|
+
cross cube cume_dist current_date current_time current_timestamp
|
|
118
|
+
current_user cursor database databases day_hour day_microsecond
|
|
119
|
+
day_minute day_second dec decimal declare default delayed delete
|
|
120
|
+
dense_rank desc describe deterministic distinct distinctrow div double
|
|
121
|
+
drop dual each else elseif empty enclosed escaped except exists exit
|
|
122
|
+
explain false fetch first_value float float4 float8 for force foreign
|
|
123
|
+
from fulltext function generated get grant group grouping groups
|
|
124
|
+
having high_priority hour_microsecond hour_minute hour_second if
|
|
125
|
+
ignore in index infile inner inout insensitive insert int int1 int2
|
|
126
|
+
int3 int4 int8 integer intersect interval into io_after_gtids
|
|
127
|
+
io_before_gtids is iterate join json_table key keys kill lag
|
|
128
|
+
last_value lateral lead leading leave left library like limit linear
|
|
129
|
+
lines load localtime localtimestamp lock long longblob longtext loop
|
|
130
|
+
low_priority master_bind master_ssl_verify_server_cert match maxvalue
|
|
131
|
+
mediumblob mediumint mediumtext middleint minute_microsecond
|
|
132
|
+
minute_second mod modifies natural not no_write_to_binlog nth_value
|
|
133
|
+
ntile null numeric of on optimize optimizer_costs option optionally
|
|
134
|
+
or order out outer outfile over partition percent_rank precision
|
|
135
|
+
primary procedure purge range rank read reads read_write real
|
|
136
|
+
recursive references regexp release rename repeat replace require
|
|
137
|
+
resignal restrict return revoke right rlike row rows row_number
|
|
138
|
+
schema schemas second_microsecond select sensitive separator set show
|
|
139
|
+
signal smallint spatial specific sql sqlexception sqlstate sqlwarning
|
|
140
|
+
sql_big_result sql_calc_found_rows sql_small_result ssl starting
|
|
141
|
+
stored straight_join system table terminated then tinyblob tinyint
|
|
142
|
+
tinytext to trailing trigger true undo union unique unlock unsigned
|
|
143
|
+
update usage use using utc_date utc_time utc_timestamp values
|
|
144
|
+
varbinary varchar varcharacter varying virtual when where while
|
|
145
|
+
window with write xor year_month zerofill
|
|
146
|
+
conversion current_role delete_domain_id do_domain_ids general
|
|
147
|
+
ignore_domain_ids ignore_server_ids master_heartbeat_period offset
|
|
148
|
+
page_checksum parse_vcol_expr ref_system_id returning slow
|
|
149
|
+
st_collect stats_auto_recalc stats_persistent stats_sample_pages
|
|
150
|
+
to_date vector
|
|
151
|
+
]).freeze
|
|
152
|
+
|
|
153
|
+
# PostgreSQL reserved key words (SQL Key Words appendix): the "reserved"
|
|
154
|
+
# entries plus the "reserved (can be function or type name)" entries —
|
|
155
|
+
# neither may appear as a bare table/column identifier.
|
|
156
|
+
POSTGRESQL_RESERVED_WORDS = Set.new(%w[
|
|
157
|
+
all analyse analyze and any array as asc asymmetric authorization
|
|
158
|
+
binary both case cast check collate collation column concurrently
|
|
159
|
+
constraint create cross current_catalog current_date current_role
|
|
160
|
+
current_schema current_time current_timestamp current_user default
|
|
161
|
+
deferrable desc distinct do else end except false fetch for foreign
|
|
162
|
+
freeze from full grant group having ilike in initially inner intersect
|
|
163
|
+
into is isnull join lateral leading left like limit localtime
|
|
164
|
+
localtimestamp natural not notnull null offset on only or order outer
|
|
165
|
+
overlaps placing primary references returning right select
|
|
166
|
+
session_user similar some symmetric system_user table tablesample
|
|
167
|
+
then to trailing true union unique user using variadic verbose when
|
|
168
|
+
where window with
|
|
169
|
+
]).freeze
|
|
170
|
+
|
|
171
|
+
# The subset of SQLite keywords (https://sqlite.org/lang_keywords.html)
|
|
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
|
|
175
|
+
# the other ~half of its keywords as identifiers via fallback (e.g.
|
|
176
|
+
# `key`, `temp`, `row`), and those are deliberately NOT quoted so output
|
|
177
|
+
# for such names stays byte-identical with previous releases — a name in
|
|
178
|
+
# this list produced a syntax error before, so quoting it cannot change
|
|
179
|
+
# any previously working output. Derived empirically; the probe is
|
|
180
|
+
# checked in as spec/adapter/identifier_quoting_spec.rb, which re-runs
|
|
181
|
+
# it against the bundled sqlite3 and asserts this exact set, so a new
|
|
182
|
+
# SQLite reserved word (as RETURNING was in 3.35) or a drifted list
|
|
183
|
+
# fails the suite instead of silently emitting broken SQL.
|
|
184
|
+
SQLITE_RESERVED_WORDS = Set.new(%w[
|
|
185
|
+
add all alter and as autoincrement between case cast check collate
|
|
186
|
+
commit constraint create current_date current_time current_timestamp
|
|
187
|
+
default deferrable delete distinct drop else escape except exists
|
|
188
|
+
foreign from group having in index insert intersect into is isnull
|
|
189
|
+
join limit not nothing notnull null on or order primary raise
|
|
190
|
+
references returning select set table then to transaction union
|
|
191
|
+
unique update using values when where
|
|
192
|
+
]).freeze
|
|
193
|
+
|
|
194
|
+
# Dialect submodules: bundle the quote character with the matching
|
|
195
|
+
# reserved-word set so an adapter opts in with a single include.
|
|
196
|
+
module Mysql
|
|
197
|
+
include IdentifierQuoting
|
|
198
|
+
|
|
199
|
+
private def identifier_quote_char
|
|
200
|
+
'`'
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private def reserved_words
|
|
204
|
+
MYSQL_RESERVED_WORDS
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
module Postgresql
|
|
209
|
+
include IdentifierQuoting
|
|
210
|
+
|
|
211
|
+
private def identifier_quote_char
|
|
212
|
+
'"'
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private def reserved_words
|
|
216
|
+
POSTGRESQL_RESERVED_WORDS
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
module Sqlite
|
|
221
|
+
include IdentifierQuoting
|
|
222
|
+
|
|
223
|
+
private def identifier_quote_char
|
|
224
|
+
'"'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private def reserved_words
|
|
228
|
+
SQLITE_RESERVED_WORDS
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -5,6 +5,7 @@ require 'open3'
|
|
|
5
5
|
module Exwiw
|
|
6
6
|
module Adapter
|
|
7
7
|
class MysqlAdapter < Base
|
|
8
|
+
include IdentifierQuoting::Mysql
|
|
8
9
|
include SqlBulkInsert
|
|
9
10
|
|
|
10
11
|
# A lazy, streaming stand-in for the materialized rows #execute used to
|
|
@@ -152,22 +153,23 @@ module Exwiw
|
|
|
152
153
|
end
|
|
153
154
|
|
|
154
155
|
# The INSERT header for this adapter. MySQL backtick-quotes the table and
|
|
155
|
-
# column identifiers
|
|
156
|
-
#
|
|
156
|
+
# column identifiers (always, to stay byte-identical with historical
|
|
157
|
+
# output). #to_bulk_insert / #write_inserts (SqlBulkInsert) append the
|
|
158
|
+
# value tuples and the trailing `;`.
|
|
157
159
|
private def insert_header(table)
|
|
158
|
-
table_name = table.name
|
|
160
|
+
table_name = force_quote_table_name(table.name)
|
|
159
161
|
if table.rails_managed?
|
|
160
|
-
"INSERT INTO
|
|
162
|
+
"INSERT INTO #{table_name} VALUES\n"
|
|
161
163
|
else
|
|
162
|
-
column_names = table.columns.map { |c|
|
|
163
|
-
"INSERT INTO
|
|
164
|
+
column_names = table.columns.map { |c| force_quote_identifier(c.name) }.join(', ')
|
|
165
|
+
"INSERT INTO #{table_name} (#{column_names}) VALUES\n"
|
|
164
166
|
end
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
def to_bulk_delete(select_query_ast, table)
|
|
168
170
|
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
|
169
171
|
|
|
170
|
-
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
|
172
|
+
sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
|
|
171
173
|
|
|
172
174
|
if select_query_ast.join_clauses.empty?
|
|
173
175
|
# Ignore filter option, because bulk delete is for cleaning before import,
|
|
@@ -205,7 +207,7 @@ module Exwiw
|
|
|
205
207
|
|
|
206
208
|
foreign_key = first_join.foreign_key
|
|
207
209
|
subquery_sql = compile_ast(subquery_ast)
|
|
208
|
-
sql += "\nWHERE #{select_query_ast.from_table_name
|
|
210
|
+
sql += "\nWHERE #{qualified_name(select_query_ast.from_table_name, foreign_key)} IN (#{subquery_sql})"
|
|
209
211
|
|
|
210
212
|
# first_join.base_where_clauses holds conditions on the outer
|
|
211
213
|
# delete-target table (from_table_name), such as a polymorphic type
|
|
@@ -240,14 +242,14 @@ module Exwiw
|
|
|
240
242
|
elsif query_ast.select_all
|
|
241
243
|
# A lifted scope JOIN brings a derived table into FROM, so a bare
|
|
242
244
|
# `*` would also project its column. Qualify to this table's own.
|
|
243
|
-
scope_clauses.any? ? "#{query_ast.from_table_name}.*" : "*"
|
|
245
|
+
scope_clauses.any? ? "#{quote_table_name(query_ast.from_table_name)}.*" : "*"
|
|
244
246
|
else
|
|
245
247
|
query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ')
|
|
246
248
|
end
|
|
247
|
-
sql += " FROM #{query_ast.from_table_name}"
|
|
249
|
+
sql += " FROM #{quote_table_name(query_ast.from_table_name)}"
|
|
248
250
|
|
|
249
251
|
query_ast.join_clauses.each do |join|
|
|
250
|
-
sql += " JOIN #{join.join_table_name} ON #{join.base_table_name
|
|
252
|
+
sql += " JOIN #{quote_table_name(join.join_table_name)} ON #{qualified_name(join.base_table_name, join.foreign_key)} = #{qualified_name(join.join_table_name, join.primary_key)}"
|
|
251
253
|
|
|
252
254
|
join.where_clauses.each do |where|
|
|
253
255
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
@@ -290,10 +292,10 @@ module Exwiw
|
|
|
290
292
|
# to `<col> IN (subquery)`.
|
|
291
293
|
private def compile_scope_join(from_table_name, where_clause, idx)
|
|
292
294
|
subquery = where_clause.value
|
|
293
|
-
projection = subquery_projection_name(subquery)
|
|
295
|
+
projection = quote_identifier(subquery_projection_name(subquery))
|
|
294
296
|
src_alias = "exwiw_scope_src_#{idx}"
|
|
295
297
|
ids_alias = "exwiw_scope_ids_#{idx}"
|
|
296
|
-
outer_key =
|
|
298
|
+
outer_key = qualified_name(from_table_name, where_clause.column_name)
|
|
297
299
|
|
|
298
300
|
"JOIN (SELECT DISTINCT #{src_alias}.#{projection} AS exwiw_scope_id " \
|
|
299
301
|
"FROM (#{compile_subquery(subquery)}) AS #{src_alias}) AS #{ids_alias} " \
|
|
@@ -304,7 +306,7 @@ module Exwiw
|
|
|
304
306
|
# Use as it is if it's a raw query
|
|
305
307
|
return where_clause if where_clause.is_a?(String)
|
|
306
308
|
|
|
307
|
-
key =
|
|
309
|
+
key = qualified_name(table_name, where_clause.column_name)
|
|
308
310
|
|
|
309
311
|
if where_clause.operator == :eq
|
|
310
312
|
values = where_clause.value.map { |v| escape_value(v) }
|
|
@@ -335,9 +337,9 @@ module Exwiw
|
|
|
335
337
|
end
|
|
336
338
|
|
|
337
339
|
inner_values = subquery.where_values.map { |v| escape_value(v) }
|
|
338
|
-
"SELECT #{subquery.table_name
|
|
339
|
-
"FROM #{subquery.table_name} " \
|
|
340
|
-
"WHERE #{subquery.table_name
|
|
340
|
+
"SELECT #{qualified_name(subquery.table_name, subquery.select_column)} " \
|
|
341
|
+
"FROM #{quote_table_name(subquery.table_name)} " \
|
|
342
|
+
"WHERE #{qualified_name(subquery.table_name, subquery.where_column)} IN (#{inner_values.join(', ')})"
|
|
341
343
|
end
|
|
342
344
|
|
|
343
345
|
# Backslash and control-character escapes, matching mysqldump. Escaping
|
|
@@ -372,14 +374,14 @@ module Exwiw
|
|
|
372
374
|
private def compile_column_name(ast, column)
|
|
373
375
|
case column
|
|
374
376
|
when Exwiw::QueryAst::ColumnValue::Plain
|
|
375
|
-
|
|
377
|
+
qualified_name(ast.from_table_name, column.name)
|
|
376
378
|
when Exwiw::QueryAst::ColumnValue::RawSql
|
|
377
379
|
column.value
|
|
378
380
|
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
|
379
381
|
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
|
380
382
|
if part.start_with?('{')
|
|
381
383
|
name = part[1..-2]
|
|
382
|
-
|
|
384
|
+
qualified_name(ast.from_table_name, name)
|
|
383
385
|
else
|
|
384
386
|
"'#{part}'"
|
|
385
387
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module Adapter
|
|
5
5
|
class PostgresqlAdapter < Base
|
|
6
|
+
include IdentifierQuoting::Postgresql
|
|
6
7
|
include SqlBulkInsert
|
|
7
8
|
|
|
8
9
|
# A lazy, streaming stand-in for the materialized rows #execute used to
|
|
@@ -158,25 +159,26 @@ module Exwiw
|
|
|
158
159
|
@logger.info(" Wrote full-database schema to #{output_path} (#{ordered_tables.size} table(s) in scope for data).")
|
|
159
160
|
end
|
|
160
161
|
|
|
161
|
-
# The INSERT header for this adapter. PostgreSQL uses bare identifiers
|
|
162
|
+
# The INSERT header for this adapter. PostgreSQL uses bare identifiers,
|
|
163
|
+
# quoted only when required (reserved word / unsafe characters).
|
|
162
164
|
# #to_bulk_insert / #write_inserts (SqlBulkInsert) append the value tuples
|
|
163
165
|
# and the trailing `;`.
|
|
164
166
|
private def insert_header(table)
|
|
165
|
-
table_name = table.name
|
|
167
|
+
table_name = quote_table_name(table.name)
|
|
166
168
|
if table.rails_managed?
|
|
167
169
|
"INSERT INTO #{table_name} VALUES\n"
|
|
168
170
|
else
|
|
169
|
-
column_names = table.columns.map(
|
|
171
|
+
column_names = table.columns.map { |c| quote_identifier(c.name) }.join(', ')
|
|
170
172
|
"INSERT INTO #{table_name} (#{column_names}) VALUES\n"
|
|
171
173
|
end
|
|
172
174
|
end
|
|
173
175
|
|
|
174
176
|
def to_copy_from_stdin(results, table)
|
|
175
177
|
header = if table.rails_managed?
|
|
176
|
-
"COPY #{table.name} FROM stdin;"
|
|
178
|
+
"COPY #{quote_table_name(table.name)} FROM stdin;"
|
|
177
179
|
else
|
|
178
|
-
column_names = table.columns.map(
|
|
179
|
-
"COPY #{table.name} (#{column_names}) FROM stdin;"
|
|
180
|
+
column_names = table.columns.map { |c| quote_identifier(c.name) }.join(', ')
|
|
181
|
+
"COPY #{quote_table_name(table.name)} (#{column_names}) FROM stdin;"
|
|
180
182
|
end
|
|
181
183
|
lines = [header]
|
|
182
184
|
results.each do |row|
|
|
@@ -203,8 +205,14 @@ module Exwiw
|
|
|
203
205
|
pk = table.primary_key
|
|
204
206
|
return nil if pk.nil? || pk.empty?
|
|
205
207
|
|
|
208
|
+
# pg_get_serial_sequence parses its first argument with identifier
|
|
209
|
+
# rules (unquoted parts are case-folded), so pass the same
|
|
210
|
+
# conditionally quoted form the extraction queries use — a bare
|
|
211
|
+
# 'Order' would fold to the nonexistent relation 'order' even though
|
|
212
|
+
# the quoted extraction just succeeded. The second argument is taken
|
|
213
|
+
# literally (no folding), so the raw column name is correct.
|
|
206
214
|
seq_name = connection
|
|
207
|
-
.exec_params("SELECT pg_get_serial_sequence($1, $2)", [table.name, pk])
|
|
215
|
+
.exec_params("SELECT pg_get_serial_sequence($1, $2)", [quote_table_name(table.name), pk])
|
|
208
216
|
.values.dig(0, 0)
|
|
209
217
|
return nil if seq_name.nil?
|
|
210
218
|
|
|
@@ -219,7 +227,7 @@ module Exwiw
|
|
|
219
227
|
def to_bulk_delete(select_query_ast, table)
|
|
220
228
|
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
|
221
229
|
|
|
222
|
-
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
|
230
|
+
sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
|
|
223
231
|
|
|
224
232
|
if select_query_ast.join_clauses.empty?
|
|
225
233
|
# Ignore filter option, because bulk delete is for cleaning before import,
|
|
@@ -264,7 +272,7 @@ module Exwiw
|
|
|
264
272
|
column_pg_type(inner_table, inner_column)
|
|
265
273
|
) ? 'text' : nil
|
|
266
274
|
subquery_sql = compile_ast(subquery_ast, select_cast_to: cast_to)
|
|
267
|
-
outer_expr =
|
|
275
|
+
outer_expr = qualified_name(outer_table, foreign_key)
|
|
268
276
|
outer_expr = "#{outer_expr}::text" if cast_to
|
|
269
277
|
sql += "\nWHERE #{outer_expr} IN (#{subquery_sql})"
|
|
270
278
|
|
|
@@ -295,17 +303,17 @@ module Exwiw
|
|
|
295
303
|
sql += if query_ast.select_all
|
|
296
304
|
# A lifted scope JOIN brings a derived table into FROM, so a bare
|
|
297
305
|
# `*` would also project its column. Qualify to this table's own.
|
|
298
|
-
scope_clauses.any? ? "#{query_ast.from_table_name}.*" : "*"
|
|
306
|
+
scope_clauses.any? ? "#{quote_table_name(query_ast.from_table_name)}.*" : "*"
|
|
299
307
|
else
|
|
300
308
|
cols = query_ast.columns.map { |col| compile_column_name(query_ast, col) }
|
|
301
309
|
cols = cols.map { |c| "#{c}::#{select_cast_to}" } if select_cast_to
|
|
302
310
|
cols.join(', ')
|
|
303
311
|
end
|
|
304
|
-
sql += " FROM #{query_ast.from_table_name}"
|
|
312
|
+
sql += " FROM #{quote_table_name(query_ast.from_table_name)}"
|
|
305
313
|
|
|
306
314
|
query_ast.join_clauses.each do |join|
|
|
307
|
-
fk_expr =
|
|
308
|
-
pk_expr =
|
|
315
|
+
fk_expr = qualified_name(join.base_table_name, join.foreign_key)
|
|
316
|
+
pk_expr = qualified_name(join.join_table_name, join.primary_key)
|
|
309
317
|
if types_need_cast?(
|
|
310
318
|
column_pg_type(join.base_table_name, join.foreign_key),
|
|
311
319
|
column_pg_type(join.join_table_name, join.primary_key)
|
|
@@ -313,7 +321,7 @@ module Exwiw
|
|
|
313
321
|
fk_expr = "#{fk_expr}::text"
|
|
314
322
|
pk_expr = "#{pk_expr}::text"
|
|
315
323
|
end
|
|
316
|
-
sql += " JOIN #{join.join_table_name} ON #{fk_expr} = #{pk_expr}"
|
|
324
|
+
sql += " JOIN #{quote_table_name(join.join_table_name)} ON #{fk_expr} = #{pk_expr}"
|
|
317
325
|
|
|
318
326
|
join.where_clauses.each do |where|
|
|
319
327
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
@@ -354,13 +362,13 @@ module Exwiw
|
|
|
354
362
|
# outer key is cast to match.
|
|
355
363
|
private def compile_scope_join(from_table_name, where_clause, idx)
|
|
356
364
|
subquery = where_clause.value
|
|
357
|
-
projection = subquery_projection_name(subquery)
|
|
365
|
+
projection = quote_identifier(subquery_projection_name(subquery))
|
|
358
366
|
src_alias = "exwiw_scope_src_#{idx}"
|
|
359
367
|
ids_alias = "exwiw_scope_ids_#{idx}"
|
|
360
368
|
|
|
361
369
|
inner_sql = compile_subquery(subquery, outer_table: from_table_name, outer_column: where_clause.column_name)
|
|
362
370
|
cast_to = subquery_cast_to(subquery, from_table_name, where_clause.column_name)
|
|
363
|
-
outer_key =
|
|
371
|
+
outer_key = qualified_name(from_table_name, where_clause.column_name)
|
|
364
372
|
outer_key = "#{outer_key}::#{cast_to}" if cast_to
|
|
365
373
|
|
|
366
374
|
"JOIN (SELECT DISTINCT #{src_alias}.#{projection} AS exwiw_scope_id " \
|
|
@@ -372,7 +380,7 @@ module Exwiw
|
|
|
372
380
|
# Use as it is if it's a raw query
|
|
373
381
|
return where_clause if where_clause.is_a?(String)
|
|
374
382
|
|
|
375
|
-
key =
|
|
383
|
+
key = qualified_name(table_name, where_clause.column_name)
|
|
376
384
|
|
|
377
385
|
if where_clause.operator == :eq
|
|
378
386
|
values = where_clause.value.map { |v| escape_value(v) }
|
|
@@ -411,11 +419,11 @@ module Exwiw
|
|
|
411
419
|
end
|
|
412
420
|
|
|
413
421
|
inner_values = subquery.where_values.map { |v| escape_value(v) }
|
|
414
|
-
select_expr =
|
|
422
|
+
select_expr = qualified_name(subquery.table_name, subquery.select_column)
|
|
415
423
|
select_expr = "#{select_expr}::#{cast_to}" if cast_to
|
|
416
424
|
"SELECT #{select_expr} " \
|
|
417
|
-
"FROM #{subquery.table_name} " \
|
|
418
|
-
"WHERE #{subquery.table_name
|
|
425
|
+
"FROM #{quote_table_name(subquery.table_name)} " \
|
|
426
|
+
"WHERE #{qualified_name(subquery.table_name, subquery.where_column)} IN (#{inner_values.join(', ')})"
|
|
419
427
|
end
|
|
420
428
|
|
|
421
429
|
private def subquery_select_target(subquery)
|
|
@@ -498,14 +506,14 @@ module Exwiw
|
|
|
498
506
|
private def compile_column_name(ast, column)
|
|
499
507
|
case column
|
|
500
508
|
when Exwiw::QueryAst::ColumnValue::Plain
|
|
501
|
-
|
|
509
|
+
qualified_name(ast.from_table_name, column.name)
|
|
502
510
|
when Exwiw::QueryAst::ColumnValue::RawSql
|
|
503
511
|
column.value
|
|
504
512
|
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
|
505
513
|
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
|
506
514
|
if part.start_with?('{')
|
|
507
515
|
name = part[1..-2]
|
|
508
|
-
|
|
516
|
+
qualified_name(ast.from_table_name, name)
|
|
509
517
|
else
|
|
510
518
|
"'#{part}'"
|
|
511
519
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module Adapter
|
|
5
5
|
class SqliteAdapter < Base
|
|
6
|
+
include IdentifierQuoting::Sqlite
|
|
6
7
|
include SqlBulkInsert
|
|
7
8
|
|
|
8
9
|
# A lazy, streaming stand-in for the materialized rows #execute used to
|
|
@@ -122,15 +123,16 @@ module Exwiw
|
|
|
122
123
|
stmt.end_with?(';') ? stmt : "#{stmt};"
|
|
123
124
|
end
|
|
124
125
|
|
|
125
|
-
# The INSERT header for this adapter. SQLite uses bare identifiers
|
|
126
|
+
# The INSERT header for this adapter. SQLite uses bare identifiers,
|
|
127
|
+
# quoted only when required (reserved word / unsafe characters).
|
|
126
128
|
# #to_bulk_insert / #write_inserts (SqlBulkInsert) append the value tuples
|
|
127
129
|
# and the trailing `;`.
|
|
128
130
|
private def insert_header(table)
|
|
129
|
-
table_name = table.name
|
|
131
|
+
table_name = quote_table_name(table.name)
|
|
130
132
|
if table.rails_managed?
|
|
131
133
|
"INSERT INTO #{table_name} VALUES\n"
|
|
132
134
|
else
|
|
133
|
-
column_names = table.columns.map(
|
|
135
|
+
column_names = table.columns.map { |c| quote_identifier(c.name) }.join(', ')
|
|
134
136
|
"INSERT INTO #{table_name} (#{column_names}) VALUES\n"
|
|
135
137
|
end
|
|
136
138
|
end
|
|
@@ -138,7 +140,7 @@ module Exwiw
|
|
|
138
140
|
def to_bulk_delete(select_query_ast, table)
|
|
139
141
|
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
|
140
142
|
|
|
141
|
-
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
|
143
|
+
sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}"
|
|
142
144
|
|
|
143
145
|
if select_query_ast.join_clauses.empty?
|
|
144
146
|
# Ignore filter option, because bulk delete is for cleaning before import,
|
|
@@ -176,7 +178,7 @@ module Exwiw
|
|
|
176
178
|
|
|
177
179
|
foreign_key = first_join.foreign_key
|
|
178
180
|
subquery_sql = compile_ast(subquery_ast)
|
|
179
|
-
sql += "\nWHERE #{select_query_ast.from_table_name
|
|
181
|
+
sql += "\nWHERE #{qualified_name(select_query_ast.from_table_name, foreign_key)} IN (#{subquery_sql})"
|
|
180
182
|
|
|
181
183
|
# first_join.base_where_clauses holds conditions on the outer
|
|
182
184
|
# delete-target table (from_table_name), such as a polymorphic type
|
|
@@ -211,14 +213,14 @@ module Exwiw
|
|
|
211
213
|
elsif query_ast.select_all
|
|
212
214
|
# A lifted scope JOIN brings a derived table into FROM, so a bare
|
|
213
215
|
# `*` would also project its column. Qualify to this table's own.
|
|
214
|
-
scope_clauses.any? ? "#{query_ast.from_table_name}.*" : "*"
|
|
216
|
+
scope_clauses.any? ? "#{quote_table_name(query_ast.from_table_name)}.*" : "*"
|
|
215
217
|
else
|
|
216
218
|
query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ')
|
|
217
219
|
end
|
|
218
|
-
sql += " FROM #{query_ast.from_table_name}"
|
|
220
|
+
sql += " FROM #{quote_table_name(query_ast.from_table_name)}"
|
|
219
221
|
|
|
220
222
|
query_ast.join_clauses.each do |join|
|
|
221
|
-
sql += " JOIN #{join.join_table_name} ON #{join.base_table_name
|
|
223
|
+
sql += " JOIN #{quote_table_name(join.join_table_name)} ON #{qualified_name(join.base_table_name, join.foreign_key)} = #{qualified_name(join.join_table_name, join.primary_key)}"
|
|
222
224
|
|
|
223
225
|
join.where_clauses.each do |where|
|
|
224
226
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
@@ -254,10 +256,10 @@ module Exwiw
|
|
|
254
256
|
# `<col> IN (subquery)`.
|
|
255
257
|
private def compile_scope_join(from_table_name, where_clause, idx)
|
|
256
258
|
subquery = where_clause.value
|
|
257
|
-
projection = subquery_projection_name(subquery)
|
|
259
|
+
projection = quote_identifier(subquery_projection_name(subquery))
|
|
258
260
|
src_alias = "exwiw_scope_src_#{idx}"
|
|
259
261
|
ids_alias = "exwiw_scope_ids_#{idx}"
|
|
260
|
-
outer_key =
|
|
262
|
+
outer_key = qualified_name(from_table_name, where_clause.column_name)
|
|
261
263
|
|
|
262
264
|
"JOIN (SELECT DISTINCT #{src_alias}.#{projection} AS exwiw_scope_id " \
|
|
263
265
|
"FROM (#{compile_subquery(subquery)}) AS #{src_alias}) AS #{ids_alias} " \
|
|
@@ -268,7 +270,7 @@ module Exwiw
|
|
|
268
270
|
# Use as it is if it's a raw query
|
|
269
271
|
return where_clause if where_clause.is_a?(String)
|
|
270
272
|
|
|
271
|
-
key =
|
|
273
|
+
key = qualified_name(table_name, where_clause.column_name)
|
|
272
274
|
|
|
273
275
|
if where_clause.operator == :eq
|
|
274
276
|
values = where_clause.value.map { |v| escape_value(v) }
|
|
@@ -299,9 +301,9 @@ module Exwiw
|
|
|
299
301
|
end
|
|
300
302
|
|
|
301
303
|
inner_values = subquery.where_values.map { |v| escape_value(v) }
|
|
302
|
-
"SELECT #{subquery.table_name
|
|
303
|
-
"FROM #{subquery.table_name} " \
|
|
304
|
-
"WHERE #{subquery.table_name
|
|
304
|
+
"SELECT #{qualified_name(subquery.table_name, subquery.select_column)} " \
|
|
305
|
+
"FROM #{quote_table_name(subquery.table_name)} " \
|
|
306
|
+
"WHERE #{qualified_name(subquery.table_name, subquery.where_column)} IN (#{inner_values.join(', ')})"
|
|
305
307
|
end
|
|
306
308
|
|
|
307
309
|
private def escape_value(value)
|
|
@@ -323,14 +325,14 @@ module Exwiw
|
|
|
323
325
|
private def compile_column_name(ast, column)
|
|
324
326
|
case column
|
|
325
327
|
when Exwiw::QueryAst::ColumnValue::Plain
|
|
326
|
-
|
|
328
|
+
qualified_name(ast.from_table_name, column.name)
|
|
327
329
|
when Exwiw::QueryAst::ColumnValue::RawSql
|
|
328
330
|
column.value
|
|
329
331
|
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
|
330
332
|
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
|
331
333
|
if part.start_with?('{')
|
|
332
334
|
name = part[1..-2]
|
|
333
|
-
|
|
335
|
+
qualified_name(ast.from_table_name, name)
|
|
334
336
|
else
|
|
335
337
|
"'#{part}'"
|
|
336
338
|
end
|
data/lib/exwiw/adapter.rb
CHANGED
|
@@ -33,6 +33,12 @@ module Exwiw
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
class Base
|
|
36
|
+
# Gives every adapter #qualified_name & co. so shared helpers below
|
|
37
|
+
# (#null_preserving) can use them; the quote-char/reserved-word hooks
|
|
38
|
+
# raise NotImplementedError until a SQL adapter includes its
|
|
39
|
+
# IdentifierQuoting dialect submodule (Mysql / Postgresql / Sqlite).
|
|
40
|
+
include IdentifierQuoting
|
|
41
|
+
|
|
36
42
|
attr_reader :connection_config
|
|
37
43
|
|
|
38
44
|
def initialize(connection_config, logger)
|
|
@@ -249,10 +255,11 @@ module Exwiw
|
|
|
249
255
|
# column itself (`column.name`) — not any other column the template may
|
|
250
256
|
# reference — so only true NULL is preserved; an empty string is a real
|
|
251
257
|
# value and is still masked. The column reference uses the same
|
|
252
|
-
# `<from_table>.<col>` form as the Plain branch
|
|
258
|
+
# `<from_table>.<col>` form as the Plain branch (qualified_name comes
|
|
259
|
+
# from IdentifierQuoting, included in Base). Shared by the SQL
|
|
253
260
|
# adapters' #compile_column_name ReplaceWith handling.
|
|
254
261
|
private def null_preserving(ast, column, masked_expr)
|
|
255
|
-
"CASE WHEN #{ast.from_table_name
|
|
262
|
+
"CASE WHEN #{qualified_name(ast.from_table_name, column.name)} IS NOT NULL THEN #{masked_expr} ELSE NULL END"
|
|
256
263
|
end
|
|
257
264
|
|
|
258
265
|
# Split an outer query's WHERE clauses into the scope id-set clauses to
|
data/lib/exwiw/version.rb
CHANGED
data/lib/exwiw.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative "exwiw/embedded_in"
|
|
|
17
17
|
require_relative "exwiw/mongodb_field"
|
|
18
18
|
require_relative "exwiw/mongodb_collection_config"
|
|
19
19
|
require_relative "exwiw/ddl_postprocessor"
|
|
20
|
+
require_relative "exwiw/adapter/identifier_quoting"
|
|
20
21
|
require_relative "exwiw/adapter"
|
|
21
22
|
require_relative "exwiw/adapter/sql_bulk_insert"
|
|
22
23
|
require_relative "exwiw/adapter/sqlite_adapter"
|
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.
|
|
4
|
+
version: 0.9.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shia
|
|
@@ -55,6 +55,7 @@ files:
|
|
|
55
55
|
- ext/exwiw/ext_json/extconf.rb
|
|
56
56
|
- lib/exwiw.rb
|
|
57
57
|
- lib/exwiw/adapter.rb
|
|
58
|
+
- lib/exwiw/adapter/identifier_quoting.rb
|
|
58
59
|
- lib/exwiw/adapter/mongodb_adapter.rb
|
|
59
60
|
- lib/exwiw/adapter/mysql_adapter.rb
|
|
60
61
|
- lib/exwiw/adapter/mysql_client.rb
|