coopy 0.6.4
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.
- 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
|