sqlconv 1.0 → 1.2.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.
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