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.
@@ -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