structured_data_to_sql 0.1.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 +7 -0
- data/LICENSE +9 -0
- data/bin/structured-data-to-sql +6 -0
- data/lib/structured_data_to_sql/cli.rb +405 -0
- data/lib/structured_data_to_sql/conversion_result.rb +94 -0
- data/lib/structured_data_to_sql/diagnostic.rb +82 -0
- data/lib/structured_data_to_sql/diagnostics_report.rb +187 -0
- data/lib/structured_data_to_sql/errors.rb +48 -0
- data/lib/structured_data_to_sql/format.rb +41 -0
- data/lib/structured_data_to_sql/io_support.rb +154 -0
- data/lib/structured_data_to_sql/json/exporter_manifest.rb +127 -0
- data/lib/structured_data_to_sql/json/json_schema_loader.rb +310 -0
- data/lib/structured_data_to_sql/json/profiles/khoros_api_export.rb +1960 -0
- data/lib/structured_data_to_sql/json/profiles.rb +14 -0
- data/lib/structured_data_to_sql/json/record_streamer.rb +477 -0
- data/lib/structured_data_to_sql/json/schema_inferrer.rb +150 -0
- data/lib/structured_data_to_sql/json/shredder.rb +241 -0
- data/lib/structured_data_to_sql/json/sql_emitter.rb +198 -0
- data/lib/structured_data_to_sql/json_converter.rb +913 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/invalid_character_report.rb +82 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/sanitizer.rb +152 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/sax_parser.rb +111 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/sql_emitter.rb +104 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/table_data_filter.rb +343 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/table_discovery.rb +651 -0
- data/lib/structured_data_to_sql/mysql_dump_xml/table_structure.rb +98 -0
- data/lib/structured_data_to_sql/mysql_dump_xml_converter.rb +4 -0
- data/lib/structured_data_to_sql/options.rb +89 -0
- data/lib/structured_data_to_sql/progress_reporter.rb +348 -0
- data/lib/structured_data_to_sql/sql_text.rb +29 -0
- data/lib/structured_data_to_sql/version.rb +5 -0
- data/lib/structured_data_to_sql/xml_converter.rb +651 -0
- data/lib/structured_data_to_sql/xml_dump_converter.rb +4 -0
- data/lib/structured_data_to_sql.rb +54 -0
- metadata +120 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module StructuredDataToSql
|
|
6
|
+
module MysqlDumpXml
|
|
7
|
+
class TableDataFilter
|
|
8
|
+
XML_CHUNK_SIZE = 64 * 1024
|
|
9
|
+
CDATA_START = "<![CDATA[".b
|
|
10
|
+
CDATA_END = "]]>".b
|
|
11
|
+
COMMENT_START = "<!--".b
|
|
12
|
+
COMMENT_END = "-->".b
|
|
13
|
+
PROCESSING_START = "<?".b
|
|
14
|
+
PROCESSING_END = "?>".b
|
|
15
|
+
TABLE_DATA_START = /<\s*table_data\b/n
|
|
16
|
+
TABLE_DATA_END = %r{<\s*/\s*table_data\b}n
|
|
17
|
+
ATTR = /\b([A-Za-z_:][\w:.-]*)\s*=\s*(['"])(.*?)\2/mn
|
|
18
|
+
SCAN_TAIL_BYTES = 128
|
|
19
|
+
|
|
20
|
+
def initialize(
|
|
21
|
+
path:,
|
|
22
|
+
table_included:,
|
|
23
|
+
event_handler:,
|
|
24
|
+
skipped_byte_handler:
|
|
25
|
+
)
|
|
26
|
+
@path = path
|
|
27
|
+
@table_included = table_included
|
|
28
|
+
@event_handler = event_handler
|
|
29
|
+
@skipped_byte_handler = skipped_byte_handler
|
|
30
|
+
@buffer = +"".b
|
|
31
|
+
@passthrough_until = nil
|
|
32
|
+
@skip_until = nil
|
|
33
|
+
@skipping_table = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def feed(chunk, &block)
|
|
37
|
+
@buffer << chunk.to_s.b
|
|
38
|
+
drain(final: false, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def finish(&block)
|
|
42
|
+
drain(final: true, &block)
|
|
43
|
+
return unless @skipping_table
|
|
44
|
+
|
|
45
|
+
raise UsageError,
|
|
46
|
+
"Missing closing </table_data> for excluded XML table #{@skipping_table.inspect} in #{@path}. " \
|
|
47
|
+
"The converter skipped that table body but could not find its end; re-export the XML or include the table to inspect the malformed block."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def drain(final:, &block)
|
|
53
|
+
loop do
|
|
54
|
+
progressed =
|
|
55
|
+
if @skipping_table
|
|
56
|
+
drain_excluded_table_body(final: final, &block)
|
|
57
|
+
elsif @passthrough_until
|
|
58
|
+
drain_passthrough_until(final: final, &block)
|
|
59
|
+
else
|
|
60
|
+
drain_normal(final: final, &block)
|
|
61
|
+
end
|
|
62
|
+
break unless progressed
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def drain_normal(final:)
|
|
67
|
+
marker = next_normal_marker
|
|
68
|
+
unless marker
|
|
69
|
+
emit_safe_prefix(final: final) { |chunk| yield chunk }
|
|
70
|
+
return false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
index, type = marker
|
|
74
|
+
if type == :table_data
|
|
75
|
+
if index.positive?
|
|
76
|
+
emit(@buffer.slice!(0, index)) { |chunk| yield chunk }
|
|
77
|
+
end
|
|
78
|
+
tag_end = start_tag_end(0)
|
|
79
|
+
unless tag_end
|
|
80
|
+
trim_oversized_partial_tag
|
|
81
|
+
return false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
tag = @buffer.slice!(0, tag_end + 1)
|
|
85
|
+
table = decoded_attribute(parse_attributes(tag)["name"])
|
|
86
|
+
included = @table_included.call(table)
|
|
87
|
+
@event_handler.call(:table_data_start, table, included)
|
|
88
|
+
emit(tag) { |chunk| yield chunk }
|
|
89
|
+
if included || self_closing_tag?(tag)
|
|
90
|
+
if self_closing_tag?(tag)
|
|
91
|
+
@event_handler.call(:table_data_end, table, included)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
@skipping_table = table
|
|
95
|
+
end
|
|
96
|
+
true
|
|
97
|
+
else
|
|
98
|
+
start, ending = skip_markers(type)
|
|
99
|
+
emit(@buffer.slice!(0, index + start.bytesize)) do |chunk|
|
|
100
|
+
yield chunk
|
|
101
|
+
end
|
|
102
|
+
@passthrough_until = ending
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def drain_passthrough_until(final:)
|
|
108
|
+
index = @buffer.index(@passthrough_until)
|
|
109
|
+
if index
|
|
110
|
+
emit(
|
|
111
|
+
@buffer.slice!(0, index + @passthrough_until.bytesize)
|
|
112
|
+
) { |chunk| yield chunk }
|
|
113
|
+
@passthrough_until = nil
|
|
114
|
+
true
|
|
115
|
+
elsif final
|
|
116
|
+
emit(@buffer.slice!(0, @buffer.bytesize)) { |chunk| yield chunk }
|
|
117
|
+
@passthrough_until = nil
|
|
118
|
+
false
|
|
119
|
+
else
|
|
120
|
+
keep = [SCAN_TAIL_BYTES, @passthrough_until.bytesize - 1].max
|
|
121
|
+
length = [@buffer.bytesize - keep, 0].max
|
|
122
|
+
if length.positive?
|
|
123
|
+
emit(@buffer.slice!(0, length)) { |chunk| yield chunk }
|
|
124
|
+
end
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def drain_excluded_table_body(final:)
|
|
130
|
+
return drain_skipped_until(final: final) if @skip_until
|
|
131
|
+
|
|
132
|
+
marker = next_excluded_marker
|
|
133
|
+
unless marker
|
|
134
|
+
discard_safe_prefix(final: final)
|
|
135
|
+
return false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
index, type = marker
|
|
139
|
+
if type == :table_data_end
|
|
140
|
+
if (tag_start = enclosing_tag_start(index))
|
|
141
|
+
tag_end = start_tag_end(tag_start)
|
|
142
|
+
unless tag_end
|
|
143
|
+
discard(@buffer.slice!(0, tag_start)) if tag_start.positive?
|
|
144
|
+
trim_oversized_partial_tag
|
|
145
|
+
return false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
discard(@buffer.slice!(0, tag_end + 1))
|
|
149
|
+
return true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
tag_end = start_tag_end(index)
|
|
153
|
+
unless tag_end
|
|
154
|
+
discard(@buffer.slice!(0, index)) if index.positive?
|
|
155
|
+
trim_oversized_partial_tag
|
|
156
|
+
return false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
discard(@buffer.slice!(0, index)) if index.positive?
|
|
160
|
+
table = @skipping_table
|
|
161
|
+
emit(@buffer.slice!(0, tag_end - index + 1)) { |chunk| yield chunk }
|
|
162
|
+
@skipping_table = nil
|
|
163
|
+
@event_handler.call(:table_data_end, table, false)
|
|
164
|
+
true
|
|
165
|
+
else
|
|
166
|
+
start, ending = skip_markers(type)
|
|
167
|
+
discard(@buffer.slice!(0, index + start.bytesize))
|
|
168
|
+
@skip_until = ending
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def drain_skipped_until(final:)
|
|
174
|
+
index = @buffer.index(@skip_until)
|
|
175
|
+
if index
|
|
176
|
+
discard(@buffer.slice!(0, index + @skip_until.bytesize))
|
|
177
|
+
@skip_until = nil
|
|
178
|
+
true
|
|
179
|
+
elsif final
|
|
180
|
+
discard(@buffer.slice!(0, @buffer.bytesize))
|
|
181
|
+
false
|
|
182
|
+
else
|
|
183
|
+
keep = [SCAN_TAIL_BYTES, @skip_until.bytesize - 1].max
|
|
184
|
+
length = [@buffer.bytesize - keep, 0].max
|
|
185
|
+
discard(@buffer.slice!(0, length)) if length.positive?
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def next_normal_marker
|
|
191
|
+
candidates = []
|
|
192
|
+
table_data = @buffer.index(TABLE_DATA_START)
|
|
193
|
+
candidates << [table_data, :table_data] if table_data
|
|
194
|
+
cdata = @buffer.index(CDATA_START)
|
|
195
|
+
candidates << [cdata, :cdata] if cdata
|
|
196
|
+
comment = @buffer.index(COMMENT_START)
|
|
197
|
+
candidates << [comment, :comment] if comment
|
|
198
|
+
processing = @buffer.index(PROCESSING_START)
|
|
199
|
+
candidates << [processing, :processing] if processing
|
|
200
|
+
candidates.min_by(&:first)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def next_excluded_marker
|
|
204
|
+
candidates = []
|
|
205
|
+
table_data_end = @buffer.index(TABLE_DATA_END)
|
|
206
|
+
candidates << [table_data_end, :table_data_end] if table_data_end
|
|
207
|
+
cdata = @buffer.index(CDATA_START)
|
|
208
|
+
candidates << [cdata, :cdata] if cdata
|
|
209
|
+
comment = @buffer.index(COMMENT_START)
|
|
210
|
+
candidates << [comment, :comment] if comment
|
|
211
|
+
processing = @buffer.index(PROCESSING_START)
|
|
212
|
+
candidates << [processing, :processing] if processing
|
|
213
|
+
candidates.min_by(&:first)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def start_tag_end(index)
|
|
217
|
+
quote = nil
|
|
218
|
+
i = index
|
|
219
|
+
while i < @buffer.bytesize
|
|
220
|
+
char = @buffer.getbyte(i)
|
|
221
|
+
if quote
|
|
222
|
+
quote = nil if char == quote
|
|
223
|
+
elsif char == 34 || char == 39
|
|
224
|
+
quote = char
|
|
225
|
+
elsif char == 62
|
|
226
|
+
return i
|
|
227
|
+
end
|
|
228
|
+
i += 1
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def enclosing_tag_start(index)
|
|
234
|
+
tag_start = @buffer.rindex("<", index - 1)
|
|
235
|
+
return nil unless tag_start
|
|
236
|
+
|
|
237
|
+
tag_end = @buffer.rindex(">", index - 1)
|
|
238
|
+
return nil if tag_end && tag_end > tag_start
|
|
239
|
+
|
|
240
|
+
tag_start
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def emit_safe_prefix(final:)
|
|
244
|
+
if final
|
|
245
|
+
unless @buffer.empty?
|
|
246
|
+
emit(@buffer.slice!(0, @buffer.bytesize)) { |chunk| yield chunk }
|
|
247
|
+
end
|
|
248
|
+
else
|
|
249
|
+
keep = SCAN_TAIL_BYTES
|
|
250
|
+
length = [@buffer.bytesize - keep, 0].max
|
|
251
|
+
if length.positive?
|
|
252
|
+
emit(@buffer.slice!(0, length)) { |chunk| yield chunk }
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def discard_safe_prefix(final:)
|
|
258
|
+
keep = final ? 0 : SCAN_TAIL_BYTES
|
|
259
|
+
length = [@buffer.bytesize - keep, 0].max
|
|
260
|
+
discard(@buffer.slice!(0, length)) if length.positive?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def emit(chunk)
|
|
264
|
+
yield chunk if chunk && !chunk.empty?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def discard(chunk)
|
|
268
|
+
return if chunk.nil? || chunk.empty?
|
|
269
|
+
|
|
270
|
+
@skipped_byte_handler.call(chunk.bytesize)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def parse_attributes(tag)
|
|
274
|
+
tag
|
|
275
|
+
.scan(ATTR)
|
|
276
|
+
.each_with_object({}) do |(name, _quote, value), attrs|
|
|
277
|
+
attrs[name] = value
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def decoded_attribute(value)
|
|
282
|
+
return "" if value.nil?
|
|
283
|
+
|
|
284
|
+
value =
|
|
285
|
+
value
|
|
286
|
+
.dup
|
|
287
|
+
.force_encoding("UTF-8")
|
|
288
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
289
|
+
unescape_xml_attribute(value)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def unescape_xml_attribute(value)
|
|
293
|
+
value.gsub(/&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/) do |entity|
|
|
294
|
+
case entity
|
|
295
|
+
when "&"
|
|
296
|
+
"&"
|
|
297
|
+
when "<"
|
|
298
|
+
"<"
|
|
299
|
+
when ">"
|
|
300
|
+
">"
|
|
301
|
+
when """
|
|
302
|
+
'"'
|
|
303
|
+
when "'"
|
|
304
|
+
"'"
|
|
305
|
+
when /\A&#(\d+);\z/
|
|
306
|
+
Regexp.last_match(1).to_i.chr(Encoding::UTF_8)
|
|
307
|
+
when /\A&#x([0-9a-fA-F]+);\z/
|
|
308
|
+
Regexp.last_match(1).to_i(16).chr(Encoding::UTF_8)
|
|
309
|
+
else
|
|
310
|
+
entity
|
|
311
|
+
end
|
|
312
|
+
rescue RangeError
|
|
313
|
+
entity
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def skip_markers(type)
|
|
318
|
+
case type
|
|
319
|
+
when :cdata
|
|
320
|
+
[CDATA_START, CDATA_END]
|
|
321
|
+
when :comment
|
|
322
|
+
[COMMENT_START, COMMENT_END]
|
|
323
|
+
when :processing
|
|
324
|
+
[PROCESSING_START, PROCESSING_END]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def self_closing_tag?(tag)
|
|
329
|
+
tag.to_s.b.sub(/>\z/n, "").rstrip.end_with?("/".b)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def trim_oversized_partial_tag
|
|
333
|
+
return if @buffer.bytesize <= XML_CHUNK_SIZE + SCAN_TAIL_BYTES
|
|
334
|
+
|
|
335
|
+
if @skipping_table
|
|
336
|
+
discard(@buffer.slice!(0, @buffer.bytesize - SCAN_TAIL_BYTES))
|
|
337
|
+
else
|
|
338
|
+
@buffer = @buffer.byteslice(1, @buffer.bytesize - 1) || +"".b
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|