coopy 0.6.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +8 -0
- data/bin/sqlite_diff +4 -0
- data/bin/sqlite_patch +4 -0
- data/bin/sqlite_rediff +4 -0
- data/lib/coopy.rb +178 -0
- data/lib/coopy/dbi_sql_wrapper.rb +89 -0
- data/lib/coopy/diff_apply_sql.rb +35 -0
- data/lib/coopy/diff_columns.rb +33 -0
- data/lib/coopy/diff_output.rb +21 -0
- data/lib/coopy/diff_output_action.rb +34 -0
- data/lib/coopy/diff_output_group.rb +40 -0
- data/lib/coopy/diff_output_raw.rb +17 -0
- data/lib/coopy/diff_output_stats.rb +45 -0
- data/lib/coopy/diff_output_table.rb +49 -0
- data/lib/coopy/diff_output_tdiff.rb +48 -0
- data/lib/coopy/diff_parser.rb +92 -0
- data/lib/coopy/diff_render_csv.rb +29 -0
- data/lib/coopy/diff_render_html.rb +74 -0
- data/lib/coopy/diff_render_log.rb +52 -0
- data/lib/coopy/row_change.rb +25 -0
- data/lib/coopy/scraperwiki_sql_wrapper.rb +8 -0
- data/lib/coopy/scraperwiki_utils.rb +23 -0
- data/lib/coopy/sequel_sql_wrapper.rb +73 -0
- data/lib/coopy/sql_compare.rb +222 -0
- data/lib/coopy/sql_wrapper.rb +34 -0
- data/lib/coopy/sqlite_sql_wrapper.rb +143 -0
- data/test/test_coopy.rb +126 -0
- metadata +86 -0
data/Rakefile
ADDED
data/bin/sqlite_diff
ADDED
data/bin/sqlite_patch
ADDED
data/bin/sqlite_rediff
ADDED
data/lib/coopy.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class Coopy
|
4
|
+
|
5
|
+
class Flavor
|
6
|
+
attr_accessor :key
|
7
|
+
attr_accessor :banner
|
8
|
+
attr_accessor :min_length
|
9
|
+
|
10
|
+
def sql_subject?
|
11
|
+
[:diff,:patch].include? key
|
12
|
+
end
|
13
|
+
|
14
|
+
def sql_object?
|
15
|
+
key == :diff
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_choose_format?
|
19
|
+
key != :patch
|
20
|
+
end
|
21
|
+
|
22
|
+
def can_set_output?
|
23
|
+
key != :patch
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_format
|
27
|
+
(key==:patch) ? :apply : :csv
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class OpenStruct
|
32
|
+
attr_accessor :format
|
33
|
+
attr_accessor :output
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.parse(flavor,args)
|
37
|
+
options = OpenStruct.new
|
38
|
+
options.format = flavor.default_format
|
39
|
+
options.output = nil
|
40
|
+
OptionParser.new do |opts|
|
41
|
+
begin
|
42
|
+
opts.banner = flavor.banner
|
43
|
+
opts.separator ""
|
44
|
+
opts.separator "Specific options"
|
45
|
+
if flavor.can_choose_format?
|
46
|
+
opts.on("-f","--format [FORMAT]", [:csv, :html, :tdiff, :apply, :stats],
|
47
|
+
"select format (csv,html,tdiff,apply,stats)") do |fmt|
|
48
|
+
options.format = fmt
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if flavor.can_set_output?
|
52
|
+
opts.on("-o", "--output [FILENAME]",
|
53
|
+
"direct output to a file") do |fname|
|
54
|
+
options.output = fname
|
55
|
+
end
|
56
|
+
end
|
57
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
58
|
+
puts opts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
opts.parse!(args)
|
62
|
+
return options
|
63
|
+
rescue
|
64
|
+
puts "#{$!} (--help for help)"
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.core(flavor,argv)
|
71
|
+
options = self.parse(flavor,argv)
|
72
|
+
|
73
|
+
if argv.length < flavor.min_length
|
74
|
+
self.parse(flavor,["--help"])
|
75
|
+
exit(1)
|
76
|
+
end
|
77
|
+
|
78
|
+
if flavor.sql_subject?
|
79
|
+
db = SQLite3::Database.new(argv[0])
|
80
|
+
sql = SqliteSqlWrapper.new(db)
|
81
|
+
end
|
82
|
+
|
83
|
+
if flavor.sql_object?
|
84
|
+
name1 = nil
|
85
|
+
name2 = nil
|
86
|
+
case argv.length
|
87
|
+
when 2
|
88
|
+
name0 = sql.get_table_names[0]
|
89
|
+
db.execute("ATTACH ? AS `__peer_ - _`",argv[1])
|
90
|
+
name1 = "main.#{name0}"
|
91
|
+
name2 = "__peer_ - _.#{name0}"
|
92
|
+
when 3
|
93
|
+
name1 = argv[1]
|
94
|
+
name2 = argv[2]
|
95
|
+
when 4
|
96
|
+
name1 = "main.#{argv[1]}"
|
97
|
+
db.execute("ATTACH ? AS __peer__",argv[2])
|
98
|
+
name2 = "__peer__.#{argv[3]}"
|
99
|
+
end
|
100
|
+
cmp = SqlCompare.new(sql,name1,name2)
|
101
|
+
else
|
102
|
+
cmp = DiffParser.new(argv[flavor.min_length-1])
|
103
|
+
end
|
104
|
+
|
105
|
+
patches = DiffOutputGroup.new
|
106
|
+
# patches << DiffOutputRaw.new
|
107
|
+
case options.format
|
108
|
+
when :html
|
109
|
+
patches << DiffRenderHtml.new
|
110
|
+
when :tdiff
|
111
|
+
patches << DiffOutputTdiff.new
|
112
|
+
when :csv
|
113
|
+
patches << DiffRenderCsv.new
|
114
|
+
when :apply
|
115
|
+
patches << DiffApplySql.new(sql,name1)
|
116
|
+
when :raw
|
117
|
+
patches << DiffOutputRaw.new
|
118
|
+
when :stats
|
119
|
+
patches << DiffOutputStats.new
|
120
|
+
else
|
121
|
+
patches << DiffRenderCsv.new
|
122
|
+
end
|
123
|
+
|
124
|
+
cmp.set_output(patches)
|
125
|
+
|
126
|
+
cmp.apply
|
127
|
+
result = patches.to_string
|
128
|
+
if result != ""
|
129
|
+
if options.output.nil?
|
130
|
+
print result
|
131
|
+
else
|
132
|
+
File.open(options.output,"w") do |f|
|
133
|
+
f << result
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
0
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.diff(argv)
|
141
|
+
flavor = Flavor.new
|
142
|
+
flavor.key = :diff
|
143
|
+
flavor.banner = "Usage: sqlite_diff [options] ver1.sqlite ver2.sqlite"
|
144
|
+
flavor.min_length = 2
|
145
|
+
self.core(flavor,argv)
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.patch(argv)
|
149
|
+
flavor = Flavor.new
|
150
|
+
flavor.key = :patch
|
151
|
+
flavor.banner = "Usage: sqlite_patch [options] db.sqlite patch.csv"
|
152
|
+
flavor.min_length = 2
|
153
|
+
self.core(flavor,argv)
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.rediff(argv)
|
157
|
+
flavor = Flavor.new
|
158
|
+
flavor.key = :rediff
|
159
|
+
flavor.banner = "Usage: sqlite_rediff [options] patch.csv"
|
160
|
+
flavor.min_length = 1
|
161
|
+
self.core(flavor,argv)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
require 'coopy/diff_output_raw'
|
166
|
+
require 'coopy/diff_output_tdiff'
|
167
|
+
require 'coopy/diff_render_html'
|
168
|
+
require 'coopy/diff_render_csv'
|
169
|
+
require 'coopy/diff_output_action'
|
170
|
+
require 'coopy/diff_output_group'
|
171
|
+
require 'coopy/diff_output_stats'
|
172
|
+
require 'coopy/diff_apply_sql'
|
173
|
+
require 'coopy/diff_parser'
|
174
|
+
|
175
|
+
require 'coopy/sqlite_sql_wrapper'
|
176
|
+
require 'coopy/sql_compare'
|
177
|
+
require 'sqlite3'
|
178
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'sql_wrapper'
|
2
|
+
require 'dbi'
|
3
|
+
|
4
|
+
class Coopy::DbiSqlWrapper < SqlWrapper
|
5
|
+
def initialize(db)
|
6
|
+
@db = db
|
7
|
+
@t = nil
|
8
|
+
@qt = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def complete_table(tbl)
|
12
|
+
return tbl unless tbl.nil?
|
13
|
+
return @t unless @t.nil?
|
14
|
+
@t = @db.tables[0]
|
15
|
+
@t
|
16
|
+
end
|
17
|
+
|
18
|
+
def quote_table(tbl)
|
19
|
+
return @db.quote(tbl) unless tbl.nil?
|
20
|
+
return @qt unless @qt.nil?
|
21
|
+
@t = @db.tables[0]
|
22
|
+
@qt = @db.quote(@t)
|
23
|
+
@qt
|
24
|
+
end
|
25
|
+
|
26
|
+
def insert(tbl,cols,vals)
|
27
|
+
tbl = quote_table(tbl)
|
28
|
+
template = cols.map{|x| '?'}.join(",")
|
29
|
+
template = "INSERT INTO #{tbl} VALUES(#{template})"
|
30
|
+
stmt = @db.prepare(template)
|
31
|
+
stmt.execute(*vals)
|
32
|
+
stmt.finish
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(tbl,cols,vals)
|
36
|
+
tbl = quote_table(tbl)
|
37
|
+
template = cols.map{|c| @db.quote(c) + ' = ?'}.join(" AND ")
|
38
|
+
template = "DELETE FROM #{tbl} WHERE #{template}"
|
39
|
+
stmt = @db.prepare(template)
|
40
|
+
stmt.execute(*vals)
|
41
|
+
stmt.finish
|
42
|
+
end
|
43
|
+
|
44
|
+
def update(tbl,set_cols,set_vals,cond_cols,cond_vals)
|
45
|
+
tbl = quote_table(tbl)
|
46
|
+
conds = cond_cols.map{|c| @db.quote(c) + ' = ?'}.join(" AND ")
|
47
|
+
sets = set_cols.map{|c| @db.quote(c) + ' = ?'}.join(", ")
|
48
|
+
template = "UPDATE #{@qt} SET #{sets} WHERE #{conds}"
|
49
|
+
v = set_vals + cond_vals
|
50
|
+
stmt = @db.prepare(template)
|
51
|
+
stmt.execute(*v)
|
52
|
+
stmt.finish
|
53
|
+
end
|
54
|
+
|
55
|
+
def transaction(&block)
|
56
|
+
@db["AutoCommit"]=false
|
57
|
+
begin
|
58
|
+
block.call
|
59
|
+
@db.commit
|
60
|
+
rescue Exception => e
|
61
|
+
@db.rollback
|
62
|
+
raise e
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def columns(tbl)
|
67
|
+
tbl = complete_table(tbl)
|
68
|
+
@db.columns(tbl)
|
69
|
+
end
|
70
|
+
|
71
|
+
def column_names(tbl)
|
72
|
+
columns(tbl).map{|c| c[:name]}
|
73
|
+
end
|
74
|
+
|
75
|
+
def enhash(cols,vals)
|
76
|
+
Hash[*cols.map{|c| c.to_sym}.zip(vals).flatten]
|
77
|
+
end
|
78
|
+
|
79
|
+
def fetch(sql,names)
|
80
|
+
@db.select_all(sql) do |row|
|
81
|
+
yield row
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def primary_key(tbl)
|
86
|
+
# don't seem to have this information? oy.
|
87
|
+
[column_names(tbl)[0]]
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'coopy/diff_output_action'
|
2
|
+
require 'coopy/sql_wrapper'
|
3
|
+
|
4
|
+
# for now, assume no schema changes, and a single table
|
5
|
+
class Coopy::DiffApplySql < DiffOutputAction
|
6
|
+
def initialize(db, name = nil)
|
7
|
+
@name = name
|
8
|
+
@db = db
|
9
|
+
end
|
10
|
+
|
11
|
+
def row_insert(rc)
|
12
|
+
cols = rc.active_columns
|
13
|
+
@db.insert(@name,
|
14
|
+
cols.map{|c| c[:title]},
|
15
|
+
cols.map{|c| rc.value_at(c)})
|
16
|
+
end
|
17
|
+
|
18
|
+
def row_delete(rc)
|
19
|
+
cols = rc.active_columns
|
20
|
+
@db.delete(@name,
|
21
|
+
cols.map{|c| c[:title]},
|
22
|
+
cols.map{|c| rc.value_at(c)})
|
23
|
+
end
|
24
|
+
|
25
|
+
def row_update(rc)
|
26
|
+
cols = rc.active_columns
|
27
|
+
touched_cols = cols.select{|c| !rc.new_value_at(c).nil?}
|
28
|
+
@db.update(@name,
|
29
|
+
touched_cols.map{|c| c[:title]},
|
30
|
+
touched_cols.map{|c| rc.new_value_at(c)},
|
31
|
+
cols.map{|c| c[:title]},
|
32
|
+
cols.map{|c| rc.value_at(c)})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class DiffColumns
|
2
|
+
attr_accessor :change_row
|
3
|
+
attr_accessor :title_row
|
4
|
+
|
5
|
+
# general
|
6
|
+
attr_accessor :column_name # *after* any column changes
|
7
|
+
attr_accessor :column_offset # *after* any column changes
|
8
|
+
attr_accessor :column_by_name
|
9
|
+
attr_accessor :column_by_offset
|
10
|
+
|
11
|
+
def update(prefix=1)
|
12
|
+
return if @title_row.nil?
|
13
|
+
@column_name = {}
|
14
|
+
@column_offset = {}
|
15
|
+
@column_by_name = {}
|
16
|
+
@column_by_offset = []
|
17
|
+
offset = -prefix
|
18
|
+
@title_row.each_with_index do |title,idx|
|
19
|
+
@column_name[idx] = title
|
20
|
+
if offset>=0
|
21
|
+
# assuming no column changes for the moment
|
22
|
+
@column_offset[idx] = offset
|
23
|
+
@column_by_name[title] = {
|
24
|
+
:title => title,
|
25
|
+
:in_offset => offset,
|
26
|
+
:diff_offset => idx
|
27
|
+
}
|
28
|
+
@column_by_offset << @column_by_name[title]
|
29
|
+
end
|
30
|
+
offset = offset+1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'coopy/diff_output'
|
2
|
+
|
3
|
+
class DiffOutputAction < DiffOutput
|
4
|
+
def row_insert(rc)
|
5
|
+
end
|
6
|
+
|
7
|
+
def row_delete(rc)
|
8
|
+
end
|
9
|
+
|
10
|
+
def row_update(rc)
|
11
|
+
end
|
12
|
+
|
13
|
+
def row_skip(rc)
|
14
|
+
end
|
15
|
+
|
16
|
+
def row_context(rc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_row(rc)
|
20
|
+
mode = rc.row_mode
|
21
|
+
case mode
|
22
|
+
when "+++"
|
23
|
+
row_insert(rc)
|
24
|
+
when "---"
|
25
|
+
row_delete(rc)
|
26
|
+
when "->"
|
27
|
+
row_update(rc)
|
28
|
+
when "..."
|
29
|
+
row_skip(rc)
|
30
|
+
when ""
|
31
|
+
row_context(rc)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'coopy/diff_columns'
|
2
|
+
require 'coopy/row_change'
|
3
|
+
|
4
|
+
class DiffOutputGroup
|
5
|
+
def initialize(*sinks)
|
6
|
+
@sinks = sinks
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(x)
|
10
|
+
@sinks = [] if @sinks.nil?
|
11
|
+
@sinks << x
|
12
|
+
end
|
13
|
+
|
14
|
+
def begin_diff
|
15
|
+
@sinks.each { |s| s.begin_diff }
|
16
|
+
end
|
17
|
+
|
18
|
+
def end_diff
|
19
|
+
@sinks.each { |s| s.end_diff }
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_row(rc)
|
23
|
+
@sinks.each { |s| s.apply_row(rc) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_string
|
27
|
+
@sinks.each do |s|
|
28
|
+
result = s.to_string
|
29
|
+
return result if result!=""
|
30
|
+
end
|
31
|
+
""
|
32
|
+
end
|
33
|
+
|
34
|
+
def want_context
|
35
|
+
return @want_context0 unless @want_context0.nil?
|
36
|
+
@want_context0 = false
|
37
|
+
@want_context0 = @sinks.each { |s| @want_context0 ||= s.want_context }
|
38
|
+
@want_context0
|
39
|
+
end
|
40
|
+
end
|