coopy 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'coopy'
4
+ Coopy.diff(ARGV)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'coopy'
4
+ Coopy.patch(ARGV)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'coopy'
4
+ Coopy.rediff(ARGV)
@@ -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,21 @@
1
+ require 'coopy/diff_columns'
2
+ require 'coopy/row_change'
3
+
4
+ class DiffOutput
5
+ def begin_diff
6
+ end
7
+
8
+ def end_diff
9
+ end
10
+
11
+ def apply_row(rc)
12
+ end
13
+
14
+ def to_string
15
+ ""
16
+ end
17
+
18
+ def want_context
19
+ true
20
+ end
21
+ 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