sqlconv 1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sqlconv +113 -90
  3. data/sqlconv.gemspec +1 -1
  4. metadata +6 -8
  5. data/.ruby-version +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 953b48afedc6e52ef6430f3d1bc1b66d6d652dfdc33c494b5950a961f157b6ec
4
- data.tar.gz: 6d6d791ba8c0611245143d0198a9ef8a2be00f84e2b5faf1d01c16a3426826e3
3
+ metadata.gz: e823f40e2647848b38ea093b7127f31ea1984ca5f178826d595e72fa7f08c71b
4
+ data.tar.gz: 6467e992cf18952ec3dc357ef60f93d6ba95d488ea9a1c8edb6d3c6efe576f29
5
5
  SHA512:
6
- metadata.gz: 9a9cfa9b993be6d3ffbeadd9deef4e6c5af05933a1d680e0e4975a335530d11029066f7b26cd76d7e2466bc809d16a15f98164fc60664ec86dab67f423d0184f
7
- data.tar.gz: 2cbe842f8898108c0b627151f6ae5014df61b5d6ea5f3d31300f11067563739bd96520cfec746d670e4951a7d0e7ee38ba55c7aed99a110e1deec4048d8d02c7
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
- def die(str)
6
- warn str
7
- exit
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
- require 'strscan'
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
- (?:(\d+)\*)?(?: # $1: repeat
51
- (?:([a-zA-Z]\w*)(\()?)?(?: # $2: function, $3: optional paren
52
- (?:(['"])(.*?)\4)? | # $4: quote, $5: literal
53
- (0) | # $6: zero
54
- ((?>[1-9]\d*))? # $7: from
55
- ((?<=\d)-|-(?=\d))? # $8: thru
56
- ((?>[1-9]\d*))? # $9: till
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
- line = (pict % ([""] * cols.size)).tr("| ", "+-")
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 "", line
122
+ puts "", ltop
77
123
  both.each do |vals|
78
124
  puts pict % vals
79
- puts line if (seen += 1) == 0
125
+ puts lmid if (seen += 1) == 0
80
126
  end
81
- puts line, "#{seen} rows displayed", ""
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 die "unable to find insert statements for the '#{tab1}' table"
165
+ into or abort "unable to find insert statements for the '#{tab1}' table"
106
166
 
107
167
  # if needed, output pipes header
108
- if mode == "pipes" && map2
109
- puts map2.gsub(',','|')
110
- elsif mode == "table"
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 die "bad sql parse: '#{line}'"
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
- max = 32
131
- sep = "+-----+-#{'-' * max}-+"
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
- puts sep
201
+ table %w[ col data], data
139
202
  exit
140
203
  end
141
204
  need.each do |item|
142
- item.text &&= ["'", item.text.gsub("'", "\\\\'"), "'"].join
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 : val)
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 die "undefined function '#{item.func}'"
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 (#{len2} defined but #{ours.size} generated)"
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
- if mode == "pipes" || mode == "table"
199
- ours.map! {|e| e == 'NULL' ? '' : e !~ /\A['"]/ ? e : e[1..-2].gsub("|","~").gsub("''", "'")}
200
- end
201
- if mode == "pipes"
202
- puts ours * "|"
203
- elsif mode == "table"
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(',') : rows[0].size.times.map {|i| "col#{i+1}"}
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
- # ==[ invoke the cli ]==
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "sqlconv"
5
- s.version = "1.0"
5
+ s.version = `grep '^VERSION' bin/sqlconv | cut -f 2 -d '"'`
6
6
  s.author = "Steve Shreeve"
7
7
  s.email = "steve.shreeve@gmail.com"
8
8
  s.summary = "Handy utility to massage MySQL dump files"
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: '1.0'
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: 2019-09-28 00:00:00.000000000 Z
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
- rubyforge_project:
46
- rubygems_version: 2.7.7
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