sqlconv 1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sqlconv +113 -90
- data/sqlconv.gemspec +1 -1
- metadata +6 -8
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e823f40e2647848b38ea093b7127f31ea1984ca5f178826d595e72fa7f08c71b
|
4
|
+
data.tar.gz: 6467e992cf18952ec3dc357ef60f93d6ba95d488ea9a1c8edb6d3c6efe576f29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e725ce923d38cdc32c5a824f07e0734ebfaa06a371c5a4ca0b1ccf671880f89d5e68b413f1f40d4900ce95fcce61a78977ac768779de0c677c1c0f90461d654b
|
7
|
+
data.tar.gz: ee0fb55e02a53abee3f1a8cebfc2be79e5001f460ff7f45f8989cbfb3fcb5e412f03f5a55833823d9cd184afb8b7d7d5c967db4f4269b9ec99e1d54e314633a1
|
data/bin/sqlconv
CHANGED
@@ -1,15 +1,59 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
VERSION = "1.2.0"
|
4
|
+
|
3
5
|
STDOUT.sync = true
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
require "censive"
|
8
|
+
require "optparse"
|
9
|
+
require "strscan"
|
10
|
+
|
11
|
+
trap("INT" ) { abort "\n" }
|
12
|
+
|
13
|
+
def die!; abort "#{File.basename($0)}: invalid usage, use -h for help"; end
|
14
|
+
|
15
|
+
OptionParser.new.instance_eval do
|
16
|
+
@banner = "usage: #{program_name} <options> <src_table>(:[sel1,sel2,...]) [dst_table][:][col1,col2,...] <dumpfile.sql or ARGF>"
|
17
|
+
|
18
|
+
on "--csv" , "Output comma separated values instead of SQL"
|
19
|
+
on "--psv" , "Output pipe separated values instead of SQL"
|
20
|
+
on "--tsv" , "Output tab separated values instead of SQL"
|
21
|
+
on "-p", "--plugin <plugin1.rb,...>", "Comma separated list of plugins"
|
22
|
+
on "-r", "--replace" , "Use 'replace into' instead of 'insert into'"
|
23
|
+
on "-s", "--show" , "Show column indexes and values for the first row"
|
24
|
+
on "-t", "--table" , "Display output as a formatted table"
|
25
|
+
|
26
|
+
on "-h", "--help" , "Show help and command usage" do Kernel.abort to_s; end
|
27
|
+
on "-v", "--version" , "Show version number" do Kernel.abort "#{program_name} #{VERSION}"; end
|
28
|
+
|
29
|
+
self
|
30
|
+
end.parse!(into: opts={}) rescue abort($!.message)
|
31
|
+
|
32
|
+
csvs = opts[:csv ] and mode = "csv"
|
33
|
+
pipe = opts[:psv ] and mode = "psv"
|
34
|
+
tabs = opts[:tsv ] and mode = "tsv"
|
35
|
+
nice = opts[:table ] and mode = "table"
|
36
|
+
repl = opts[:replace] and mode = "replace"
|
37
|
+
show = opts[:show ] and mode = "show"
|
38
|
+
plug = opts[:plugin].to_s.downcase.split(",")
|
39
|
+
|
40
|
+
die! if [csvs, pipe, tabs, nice, repl, show].compact.size > 1
|
41
|
+
|
42
|
+
if ARGV.shift =~ /^([a-z][-\w]*):?(.+)?$/i
|
43
|
+
tab1 = $1
|
44
|
+
map1 = $2
|
8
45
|
end
|
9
46
|
|
10
|
-
|
47
|
+
if !ARGV.empty? and !File.exist?(ARGV.first)
|
48
|
+
if ARGV.shift =~ /^((?>[a-z]?[-\w]*)(?::|$))?(.+)?$/i
|
49
|
+
$1.to_s.size > 0 and tab2 = $1.chomp(":")
|
50
|
+
$2.to_s.size > 0 and map2 = $2.squeeze(",").sub(/^,+/,"").sub(/,+$/,"")
|
51
|
+
end
|
52
|
+
die! if $0.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
# ==[ Helpers ]==
|
11
56
|
|
12
|
-
# parsing helpers
|
13
57
|
class StringScanner
|
14
58
|
def scan_for(regx)
|
15
59
|
data = scan_until(Regexp === regx ? regx : /#{regx}/)
|
@@ -46,16 +90,15 @@ Selector = Struct.new(:want, :func, :text, :zero, :thru, :reps, :from, :till)
|
|
46
90
|
# convert user request into selectors
|
47
91
|
def grok(want)
|
48
92
|
(want || "1-").strip.split(/\s*,\s*/).map do |item|
|
49
|
-
item =~ %r!^
|
50
|
-
(
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
)$
|
93
|
+
item =~ %r!^(?:(\d+)\*)?(?:(?:( # $1: repeat
|
94
|
+
(?:\d+(?=\())|[a-zA-Z]\w*) # $2: function name
|
95
|
+
(\()?)?(?: # $3: optional paren
|
96
|
+
(?:(['"])(.*?)\4)? | # $4: quote, $5: literal
|
97
|
+
(0) | # $6: zero
|
98
|
+
((?>[1-9]\d*))? # $7: from
|
99
|
+
((?<=\d)-|-(?=\d))? # $8: thru
|
100
|
+
((?>[1-9]\d*))? # $9: till
|
101
|
+
)\)?)$
|
59
102
|
!iox or raise "invalid selector item '#{item}'"
|
60
103
|
Selector.new(*$~.values_at(0, 2, 5, 6, 8), *$~.values_at(1, 7, 9).map {|e| e&.to_i })
|
61
104
|
end or raise "invalid selector '#{want}'"
|
@@ -65,20 +108,37 @@ end
|
|
65
108
|
def table(cols, rows)
|
66
109
|
cols.is_a?(Array) && cols.size > 0 or return
|
67
110
|
rows.is_a?(Array) && rows.size > 0 or return
|
68
|
-
join = "
|
111
|
+
join = " │ "
|
69
112
|
both = [cols] + rows
|
70
113
|
flip = both.transpose
|
71
114
|
wide = flip.map {|row| row.map {|col| col.to_s.size }.max }
|
72
115
|
pict = wide.map {|len| "%-#{len}.#{len}s" }.join(join)
|
73
116
|
pict = [join, pict, join].join.strip
|
74
|
-
|
117
|
+
base = (pict % ([""] * cols.size))[1...-1]
|
118
|
+
ltop = "┌" + base.tr("│ ", "┬─") + "┐"
|
119
|
+
lmid = "├" + base.tr("│ ", "┼─") + "┤"
|
120
|
+
lbot = "└" + base.tr("│ ", "┴─") + "┘"
|
75
121
|
seen = -1
|
76
|
-
puts "",
|
122
|
+
puts "", ltop
|
77
123
|
both.each do |vals|
|
78
124
|
puts pict % vals
|
79
|
-
puts
|
125
|
+
puts lmid if (seen += 1) == 0
|
80
126
|
end
|
81
|
-
puts
|
127
|
+
puts lbot, "#{seen} rows displayed", ""
|
128
|
+
end
|
129
|
+
|
130
|
+
def escape(str)
|
131
|
+
str =~ /\A(\d+|null)\z/i ? $1 : %|'#{str.gsub("'", "\\\\'")}'|
|
132
|
+
end
|
133
|
+
|
134
|
+
def unescape(str, nulls=false)
|
135
|
+
str =~ /\A['"]/ and return str[1..-2].gsub("|","~").gsub("''", "'")
|
136
|
+
str == "NULL" and return "" unless nulls
|
137
|
+
str
|
138
|
+
end
|
139
|
+
|
140
|
+
def unescape!(str)
|
141
|
+
unescape(str, true)
|
82
142
|
end
|
83
143
|
|
84
144
|
# convert the insert statements
|
@@ -102,13 +162,20 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
102
162
|
# find source table
|
103
163
|
data.string = dump.read # dump.read(5000) # TODO: Add streaming support
|
104
164
|
into = data.scan_for(/insert into (['"`]?)#{tab1}\1 values /io)
|
105
|
-
into or
|
165
|
+
into or abort "unable to find insert statements for the '#{tab1}' table"
|
106
166
|
|
107
167
|
# if needed, output pipes header
|
108
|
-
|
109
|
-
|
110
|
-
|
168
|
+
case mode
|
169
|
+
when "psv", "tsv"
|
170
|
+
puts map2.gsub(",", mode == "psv" ? "|" : "\t") if map2
|
171
|
+
lean = true
|
172
|
+
when "csv"
|
173
|
+
$csv = Censive.writer(out: $stdout)
|
174
|
+
$csv << map2.split(",") if map2
|
175
|
+
lean = true
|
176
|
+
when "table"
|
111
177
|
rows = []
|
178
|
+
lean = true
|
112
179
|
end
|
113
180
|
|
114
181
|
# process each line
|
@@ -117,7 +184,8 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
117
184
|
# parse insert statements
|
118
185
|
if data.scan_str("(") or data.scan_str(into + "(")
|
119
186
|
cols = data.scan_while(/('.*?(?<!\\)'|(?>[^',()]+)|,)/, 2)
|
120
|
-
cols.empty? and
|
187
|
+
cols.empty? and abort "bad sql parse: '#{line}'"
|
188
|
+
cols.map! {|item| unescape(item)} if lean
|
121
189
|
data.scan(/\)[;,]\s*/)
|
122
190
|
else
|
123
191
|
break
|
@@ -127,19 +195,14 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
127
195
|
unless len1
|
128
196
|
len1 = cols.size
|
129
197
|
if mode == "show"
|
130
|
-
|
131
|
-
|
132
|
-
puts sep, "| col | %-*.*s |" % [max, max, 'data'], sep
|
133
|
-
len1.times do |pos|
|
134
|
-
val = cols[pos]
|
135
|
-
val[max-3..-1] = '...' if val.size > max
|
136
|
-
puts "| %3d | %-*.*s | " % [pos + 1, max, max, val]
|
198
|
+
data = cols.map.with_index do |data, i|
|
199
|
+
[i + 1, data.size > 32 ? data[...-3] + "..." : data]
|
137
200
|
end
|
138
|
-
|
201
|
+
table %w[ col data], data
|
139
202
|
exit
|
140
203
|
end
|
141
204
|
need.each do |item|
|
142
|
-
item.text &&=
|
205
|
+
item.text &&= escape(item.text) unless lean
|
143
206
|
if (len2 = [item.from, item.till, 0].compact.max) > len1
|
144
207
|
warn "selector '#{item.want}' referenced source column #{len2}, but only #{len1} are defined"
|
145
208
|
cols &&= nil
|
@@ -159,9 +222,14 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
159
222
|
case item.func
|
160
223
|
when "rand" then ours.push("'random number here!'")
|
161
224
|
when "n","null" then ours.push("null")
|
162
|
-
when "z" then ours.push((val = cols[item.from-1]) == "NULL" ? 0
|
225
|
+
when "z" then ours.push((val = cols[item.from -1]) == "NULL" ? 0 : val)
|
226
|
+
when /^(\d+)$/
|
227
|
+
val = cols[item.func.to_i - 1]
|
228
|
+
val = unescape(val) unless lean
|
229
|
+
val = val[0, item.from]
|
230
|
+
ours.push(val)
|
163
231
|
else
|
164
|
-
defined?(item.func) == "method" or
|
232
|
+
defined?(item.func) == "method" or abort "undefined function '#{item.func}'"
|
165
233
|
ours.push *(send item.func, *Array[cols[item.from-1]])
|
166
234
|
end
|
167
235
|
when item.text # literal
|
@@ -186,7 +254,7 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
186
254
|
# perform one-time check on destination column counts
|
187
255
|
unless len2
|
188
256
|
if map2 and (len2 = map2.split(",").size) != ours.size
|
189
|
-
warn "destination column mismatch (#{
|
257
|
+
warn "destination column mismatch (#{ours.size} sourced but #{len2} targeted)"
|
190
258
|
cols &&= nil
|
191
259
|
else
|
192
260
|
len2 = ours.size
|
@@ -195,67 +263,22 @@ def conv(tab1, map1, tab2, map2, mode, dump)
|
|
195
263
|
end
|
196
264
|
|
197
265
|
# generate output
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
rows << ours.dup
|
205
|
-
else
|
206
|
-
puts [pref, ours * ",", ");"].join
|
266
|
+
case mode
|
267
|
+
when "psv" then puts ours * "|"
|
268
|
+
when "tsv" then puts ours * "\t"
|
269
|
+
when "csv" then $csv << ours
|
270
|
+
when "table" then rows << ours.dup
|
271
|
+
else puts [pref, ours * ",", ");"].join
|
207
272
|
end
|
208
273
|
end
|
209
274
|
|
210
275
|
# output table
|
211
276
|
if mode == "table"
|
212
|
-
cols = map2 ? map2.split(
|
277
|
+
cols = map2 ? map2.split(",") : rows[0].size.times.map {|i| "col#{i+1}"}
|
213
278
|
table cols, rows
|
214
279
|
end
|
215
280
|
end
|
216
281
|
|
217
|
-
# ==[
|
218
|
-
|
219
|
-
argv = -1
|
220
|
-
ARGV.size.times do
|
221
|
-
if ARGV[argv += 1] == "-x"
|
222
|
-
begin
|
223
|
-
require plugin = ARGV[argv += 1]
|
224
|
-
rescue LoadError
|
225
|
-
die "unable to load the '#{plugin}' plugin"
|
226
|
-
end
|
227
|
-
ARGV.slice!((argv -= 2) + 1, 2)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
# mutually exclusive options
|
232
|
-
if ARGV.delete "-s" then mode = "show"
|
233
|
-
elsif ARGV.delete "-r" then mode = "replace"
|
234
|
-
elsif ARGV.delete "-p" then mode = "pipes"
|
235
|
-
elsif ARGV.delete "-t" then mode = "table"; end
|
236
|
-
|
237
|
-
if ARGV.shift =~ /^([a-z][-\w]*):?(.+)?$/
|
238
|
-
tab1 = $1
|
239
|
-
map1 = $2
|
240
|
-
end
|
241
|
-
|
242
|
-
if ARGV.size > 0 and !File.exists?(ARGV.first)
|
243
|
-
if ARGV.shift =~ /^((?>[a-z]?[-\w]*)(?::|$))?(.+)?$/
|
244
|
-
$1.to_s.size > 0 and tab2 = $1.chomp(":")
|
245
|
-
$2.to_s.size > 0 and map2 = $2.squeeze(',').sub(/^,+/,'').sub(/,+$/,'')
|
246
|
-
end
|
247
|
-
tab1 = nil if $0.size == 0 # no match, show usage
|
248
|
-
end
|
249
|
-
|
250
|
-
tab1 or die [
|
251
|
-
"Usage: #{File.basename $0} <options> " +
|
252
|
-
"<src_table>(:[sel1,sel2,...]) " +
|
253
|
-
"[dst_table][:][col1,col2,...] file",
|
254
|
-
" -p (output pipe separated values instead of SQL",
|
255
|
-
" -r (use 'replace into' instead of 'insert into')",
|
256
|
-
" -s (show column indexes and values for the first row)",
|
257
|
-
" -t (display output as a formatted table)",
|
258
|
-
" -x <plugin1.rb> [-x <plugin2.rb>]...]",
|
259
|
-
] * "\n"
|
282
|
+
# ==[ Let 'er rip! ]==
|
260
283
|
|
261
284
|
conv tab1, map1, tab2 || tab1, map2, mode, ARGF
|
data/sqlconv.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqlconv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Shreeve
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Allows mapping columns from a source to a destination table
|
14
14
|
email: steve.shreeve@gmail.com
|
@@ -17,7 +17,6 @@ executables:
|
|
17
17
|
extensions: []
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
|
-
- ".ruby-version"
|
21
20
|
- Gemfile
|
22
21
|
- LICENSE
|
23
22
|
- README.md
|
@@ -27,7 +26,7 @@ homepage: https://github.com/shreeve/sqlconv
|
|
27
26
|
licenses:
|
28
27
|
- MIT
|
29
28
|
metadata: {}
|
30
|
-
post_install_message:
|
29
|
+
post_install_message:
|
31
30
|
rdoc_options: []
|
32
31
|
require_paths:
|
33
32
|
- lib
|
@@ -42,9 +41,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
41
|
- !ruby/object:Gem::Version
|
43
42
|
version: '0'
|
44
43
|
requirements: []
|
45
|
-
|
46
|
-
|
47
|
-
signing_key:
|
44
|
+
rubygems_version: 3.4.8
|
45
|
+
signing_key:
|
48
46
|
specification_version: 4
|
49
47
|
summary: Handy utility to massage MySQL dump files
|
50
48
|
test_files: []
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.5
|