coopy 0.6.4.1 → 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +7 -0
- data/LICENSE.md +22 -0
- data/README.md +59 -0
- data/Rakefile +4 -6
- data/coopy.gemspec +26 -0
- data/lib/coopy.rb +32 -175
- data/lib/coopy/alignment.rb +260 -0
- data/lib/coopy/bag.rb +17 -0
- data/lib/coopy/cell_info.rb +24 -0
- data/lib/coopy/change_type.rb +10 -0
- data/lib/coopy/compare_flags.rb +62 -0
- data/lib/coopy/compare_table.rb +327 -0
- data/lib/coopy/coopy.rb +22 -0
- data/lib/coopy/cross_match.rb +10 -0
- data/lib/coopy/csv_table.rb +51 -0
- data/lib/coopy/diff_render.rb +307 -0
- data/lib/coopy/index.rb +73 -0
- data/lib/coopy/index_item.rb +17 -0
- data/lib/coopy/index_pair.rb +72 -0
- data/lib/coopy/mover.rb +123 -0
- data/lib/coopy/ordering.rb +27 -0
- data/lib/coopy/row.rb +9 -0
- data/lib/coopy/simple_cell.rb +15 -0
- data/lib/coopy/simple_table.rb +144 -0
- data/lib/coopy/simple_view.rb +36 -0
- data/lib/coopy/table.rb +44 -0
- data/lib/coopy/table_comparison_state.rb +33 -0
- data/lib/coopy/table_diff.rb +634 -0
- data/lib/coopy/table_text.rb +14 -0
- data/lib/coopy/table_view.rb +31 -0
- data/lib/coopy/unit.rb +53 -0
- data/lib/coopy/version.rb +3 -0
- data/lib/coopy/view.rb +34 -0
- data/spec/fixtures/bridges.html +10 -0
- data/spec/fixtures/bridges_diff.csv +8 -0
- data/spec/fixtures/bridges_new.csv +9 -0
- data/spec/fixtures/bridges_old.csv +9 -0
- data/spec/fixtures/planetary_bodies.html +22 -0
- data/spec/fixtures/planetary_bodies_diff.csv +19 -0
- data/spec/fixtures/planetary_bodies_new.csv +20 -0
- data/spec/fixtures/planetary_bodies_old.csv +19 -0
- data/spec/fixtures/quote_me.csv +10 -0
- data/spec/fixtures/quote_me2.csv +11 -0
- data/spec/integration/table_diff_spec.rb +57 -0
- data/spec/libs/compare_flags_spec.rb +40 -0
- data/spec/libs/coopy_spec.rb +14 -0
- data/spec/libs/ordering_spec.rb +28 -0
- data/spec/libs/unit_spec.rb +31 -0
- data/spec/spec_helper.rb +29 -0
- metadata +153 -46
- data/bin/sqlite_diff +0 -4
- data/bin/sqlite_patch +0 -4
- data/bin/sqlite_rediff +0 -4
- data/lib/coopy/dbi_sql_wrapper.rb +0 -89
- data/lib/coopy/diff_apply_sql.rb +0 -35
- data/lib/coopy/diff_columns.rb +0 -33
- data/lib/coopy/diff_output.rb +0 -21
- data/lib/coopy/diff_output_action.rb +0 -34
- data/lib/coopy/diff_output_group.rb +0 -40
- data/lib/coopy/diff_output_raw.rb +0 -17
- data/lib/coopy/diff_output_stats.rb +0 -45
- data/lib/coopy/diff_output_table.rb +0 -49
- data/lib/coopy/diff_output_tdiff.rb +0 -48
- data/lib/coopy/diff_parser.rb +0 -92
- data/lib/coopy/diff_render_csv.rb +0 -29
- data/lib/coopy/diff_render_html.rb +0 -74
- data/lib/coopy/diff_render_log.rb +0 -52
- data/lib/coopy/row_change.rb +0 -25
- data/lib/coopy/scraperwiki_sql_wrapper.rb +0 -8
- data/lib/coopy/scraperwiki_utils.rb +0 -23
- data/lib/coopy/sequel_sql_wrapper.rb +0 -73
- data/lib/coopy/sql_compare.rb +0 -222
- data/lib/coopy/sql_wrapper.rb +0 -34
- data/lib/coopy/sqlite_sql_wrapper.rb +0 -143
- data/test/test_coopy.rb +0 -126
data/lib/coopy/row_change.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
class RowChange
|
2
|
-
attr_accessor :row_mode
|
3
|
-
attr_accessor :cells
|
4
|
-
attr_accessor :columns
|
5
|
-
attr_accessor :key
|
6
|
-
|
7
|
-
def initialize(row_mode,cells)
|
8
|
-
@row_mode = row_mode
|
9
|
-
@cells = cells
|
10
|
-
@key = nil
|
11
|
-
end
|
12
|
-
|
13
|
-
def active_columns
|
14
|
-
return [] if @columns.nil?
|
15
|
-
@columns.column_by_offset
|
16
|
-
end
|
17
|
-
|
18
|
-
def value_at(column)
|
19
|
-
@cells[column[:in_offset]][:value]
|
20
|
-
end
|
21
|
-
|
22
|
-
def new_value_at(column)
|
23
|
-
@cells[column[:in_offset]][:new_value]
|
24
|
-
end
|
25
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
def link_tables(watch_scraper,watch_tables)
|
2
|
-
sql = ScraperwikiSqlWrapper.new(ScraperWiki)
|
3
|
-
watch_tables.each { |tbl| sql.copy_table_structure(watch_scraper,tbl) }
|
4
|
-
end
|
5
|
-
|
6
|
-
class CoopyResult
|
7
|
-
attr_accessor :html
|
8
|
-
end
|
9
|
-
|
10
|
-
def sync_table(watch_scraper,tbl,keys)
|
11
|
-
sql = ScraperwikiSqlWrapper.new(ScraperWiki)
|
12
|
-
sql.set_primary_key(keys) unless keys.nil?
|
13
|
-
cmp = SqlCompare.new(sql,"main.#{tbl}","#{watch_scraper}.#{tbl}")
|
14
|
-
sinks = DiffOutputGroup.new
|
15
|
-
render = DiffRenderHtml.new
|
16
|
-
sinks << render
|
17
|
-
sinks << DiffApplySql.new(sql,"main.#{tbl}")
|
18
|
-
cmp.set_output(sinks)
|
19
|
-
cmp.apply
|
20
|
-
result = CoopyResult.new
|
21
|
-
result.html = render.to_string
|
22
|
-
result
|
23
|
-
end
|
@@ -1,73 +0,0 @@
|
|
1
|
-
require 'sql_wrapper'
|
2
|
-
require 'sequel'
|
3
|
-
|
4
|
-
class SequelSqlBare < SqlWrapper
|
5
|
-
def initialize(db)
|
6
|
-
@db = db
|
7
|
-
@tname = nil
|
8
|
-
@t = nil
|
9
|
-
end
|
10
|
-
|
11
|
-
def sync_table(tbl)
|
12
|
-
tbl = @tname if tbl.nil?
|
13
|
-
tbl = @db.tables[0] if tbl.nil?
|
14
|
-
return @t if tbl==@tname
|
15
|
-
@tname = tbl
|
16
|
-
@t = @db[tbl]
|
17
|
-
end
|
18
|
-
|
19
|
-
def enhash(cols,vals)
|
20
|
-
Hash[*cols.map{|c| c.to_sym}.zip(vals).flatten]
|
21
|
-
end
|
22
|
-
|
23
|
-
def insert(tbl,cols,vals)
|
24
|
-
sync_table(tbl)
|
25
|
-
@t.insert(enhash(cols,vals))
|
26
|
-
end
|
27
|
-
|
28
|
-
def delete(tbl,cols,vals)
|
29
|
-
sync_table(tbl)
|
30
|
-
@t.filter(enhash(cols,vals)).delete
|
31
|
-
end
|
32
|
-
|
33
|
-
def update(tbl,set_cols,set_vals,cond_cols,cond_vals)
|
34
|
-
sync_table(tbl)
|
35
|
-
@t.filter(enhash(cond_cols,cond_vals)).update(enhash(set_cols,set_vals))
|
36
|
-
end
|
37
|
-
|
38
|
-
def transaction(&block)
|
39
|
-
@db.transaction(&block)
|
40
|
-
end
|
41
|
-
|
42
|
-
def columns(tbl)
|
43
|
-
sync_table(tbl)
|
44
|
-
@db.schema(@tname)
|
45
|
-
end
|
46
|
-
|
47
|
-
def column_names(tbl)
|
48
|
-
columns(tbl).map{|x| x[0]}
|
49
|
-
end
|
50
|
-
|
51
|
-
def primary_key(tbl)
|
52
|
-
cols = columns(tbl)
|
53
|
-
cols.select{|x| x[1][:primary_key]}.map{|x| x[0]}
|
54
|
-
end
|
55
|
-
|
56
|
-
def index(tbl)
|
57
|
-
key = primary_key(tbl)
|
58
|
-
@t.select(*key)
|
59
|
-
end
|
60
|
-
|
61
|
-
def fetch(sql,names)
|
62
|
-
@db.fetch(sql) do |row|
|
63
|
-
yield names.map{|n| row[n]}
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
|
69
|
-
class SequelSqlWrapper < SequelSqlBare
|
70
|
-
def initialize(*params)
|
71
|
-
super(Sequel.connect(*params))
|
72
|
-
end
|
73
|
-
end
|
data/lib/coopy/sql_compare.rb
DELETED
@@ -1,222 +0,0 @@
|
|
1
|
-
require 'coopy/diff_columns'
|
2
|
-
require 'coopy/row_change'
|
3
|
-
|
4
|
-
class SqlCompare
|
5
|
-
def initialize(db1,db2)
|
6
|
-
@db1 = db
|
7
|
-
@db2 = db2
|
8
|
-
@table1 = nil
|
9
|
-
@table2 = nil
|
10
|
-
@single_db = false
|
11
|
-
raise "not implemented yet"
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize(db,table1,table2)
|
15
|
-
@db1 = db
|
16
|
-
@db2 = db.clone
|
17
|
-
@table1 = table1
|
18
|
-
@table2 = table2
|
19
|
-
@single_db = true
|
20
|
-
end
|
21
|
-
|
22
|
-
def set_output(patch)
|
23
|
-
@patch = patch
|
24
|
-
end
|
25
|
-
|
26
|
-
def apply
|
27
|
-
apply_single
|
28
|
-
end
|
29
|
-
|
30
|
-
# We are not implementing full comparison, just an adequate subset
|
31
|
-
# for easy cases (a table with a trustworthy primary key, and constant
|
32
|
-
# columns). Make sure we are not trying to do something we're not ready
|
33
|
-
# for.
|
34
|
-
def validate_schema
|
35
|
-
all_cols1 = @db1.column_names(@table1)
|
36
|
-
all_cols2 = @db2.column_names(@table2)
|
37
|
-
if all_cols1 != all_cols2
|
38
|
-
raise "Columns do not match, please use full coopy toolbox"
|
39
|
-
end
|
40
|
-
|
41
|
-
key_cols1 = @db1.primary_key(@table1)
|
42
|
-
key_cols2 = @db2.primary_key(@table2)
|
43
|
-
if key_cols1 != key_cols2
|
44
|
-
raise "Primary keys do not match, please use full coopy toolbox"
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def keyify(lst)
|
49
|
-
lst.map{|x| x.to_s}.join("___")
|
50
|
-
end
|
51
|
-
|
52
|
-
# When working within a single database, we can delegate more work to SQL.
|
53
|
-
# So we specialize this case.
|
54
|
-
def apply_single
|
55
|
-
validate_schema
|
56
|
-
|
57
|
-
# Prepare some lists of columns.
|
58
|
-
key_cols = @db1.primary_key(@table1)
|
59
|
-
data_cols = @db1.except_primary_key(@table1)
|
60
|
-
all_cols = @db1.column_names(@table1)
|
61
|
-
|
62
|
-
# Let our public know we are beginning.
|
63
|
-
@patch.begin_diff
|
64
|
-
|
65
|
-
# Advertise column names.
|
66
|
-
@rc_columns = DiffColumns.new
|
67
|
-
@rc_columns.title_row = all_cols
|
68
|
-
@rc_columns.update(0)
|
69
|
-
cells = all_cols.map{|v| { :txt => v, :value => v, :cell_mode => "" }}
|
70
|
-
rc = RowChange.new("@@",cells)
|
71
|
-
@patch.apply_row(rc)
|
72
|
-
|
73
|
-
# If requested, we will be providing context rows around changed rows.
|
74
|
-
# This is not a natural thing to do with SQL, so we do it only on request.
|
75
|
-
# When requested, we need to buffer row changes.
|
76
|
-
@pending_rcs = []
|
77
|
-
|
78
|
-
# Prepare some useful SQL fragments to assemble later.
|
79
|
-
sql_table1 = @db1.quote_table(@table1)
|
80
|
-
sql_table2 = @db1.quote_table(@table2)
|
81
|
-
sql_key_cols = key_cols.map{|c| @db1.quote_column(c)}.join(",")
|
82
|
-
sql_all_cols = all_cols.map{|c| @db1.quote_column(c)}.join(",")
|
83
|
-
sql_key_match = key_cols.map{|c| @db1.quote_column(c)}.map{|c| "#{sql_table1}.#{c} IS #{sql_table2}.#{c}"}.join(" AND ")
|
84
|
-
sql_data_mismatch = data_cols.map{|c| @db1.quote_column(c)}.map{|c| "#{sql_table1}.#{c} IS NOT #{sql_table2}.#{c}"}.join(" OR ")
|
85
|
-
|
86
|
-
# For one query we will need to interleave columns from two tables. For
|
87
|
-
# portability we need to give these columns distinct names.
|
88
|
-
weave = all_cols.map{|c| [[sql_table1,@db1.quote_column(c)],
|
89
|
-
[sql_table2,@db2.quote_column(c)]]}.flatten(1)
|
90
|
-
dbl_cols = weave.map{|c| "#{c[0]}.#{c[1]}"}
|
91
|
-
sql_dbl_cols = weave.map{|c| "#{c[0]}.#{c[1]} AS #{c[0].gsub(/[^a-zA-Z0-9]/,'_')}_#{c[1].gsub(/[^a-zA-Z0-9]/,'_')}"}.join(",")
|
92
|
-
|
93
|
-
# Prepare a map of primary key offsets.
|
94
|
-
keys_in_all_cols = key_cols.each.map{|c| all_cols.index(c)}
|
95
|
-
keys_in_dbl_cols = keys_in_all_cols.map{|x| 2*x}
|
96
|
-
|
97
|
-
# Find rows in table2 that are not in table1.
|
98
|
-
sql = "SELECT #{sql_all_cols} FROM #{sql_table2} WHERE NOT EXISTS (SELECT 1 FROM #{sql_table1} WHERE #{sql_key_match})"
|
99
|
-
apply_inserts(sql,all_cols,keys_in_all_cols)
|
100
|
-
|
101
|
-
# Find rows in table1 and table2 that differ while having the same primary
|
102
|
-
# key.
|
103
|
-
sql = "SELECT #{sql_dbl_cols} FROM #{sql_table1} INNER JOIN #{sql_table2} ON #{sql_key_match} WHERE #{sql_data_mismatch}"
|
104
|
-
apply_updates(sql,dbl_cols,keys_in_dbl_cols)
|
105
|
-
|
106
|
-
# Find rows that are in table1 but not table2
|
107
|
-
sql = "SELECT #{sql_all_cols} FROM #{sql_table1} WHERE NOT EXISTS (SELECT 1 FROM #{sql_table2} WHERE #{sql_key_match})"
|
108
|
-
apply_deletes(sql,all_cols,keys_in_all_cols)
|
109
|
-
|
110
|
-
# If we are supposed to provide context, we need to deal with row order.
|
111
|
-
if @patch.want_context
|
112
|
-
sql = "SELECT #{sql_all_cols}, 0 AS __coopy_tag__ FROM #{sql_table1} UNION SELECT #{sql_all_cols}, 1 AS __coopy_tag__ FROM #{sql_table2} ORDER BY #{sql_key_cols}, __coopy_tag__"
|
113
|
-
apply_with_context(sql,all_cols,keys_in_all_cols)
|
114
|
-
end
|
115
|
-
|
116
|
-
# Done!
|
117
|
-
@patch.end_diff
|
118
|
-
end
|
119
|
-
|
120
|
-
|
121
|
-
def apply_inserts(sql,all_cols,keys_in_all_cols)
|
122
|
-
@db1.fetch(sql,all_cols) do |row|
|
123
|
-
cells = row.map{|v| { :txt => v, :value => v, :cell_mode => "" }}
|
124
|
-
rc = RowChange.new("+++",cells)
|
125
|
-
apply_rc(rc,row,keys_in_all_cols)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
|
130
|
-
def apply_updates(sql,dbl_cols,keys_in_dbl_cols)
|
131
|
-
@db1.fetch(sql,dbl_cols) do |row|
|
132
|
-
pairs = row.enum_for(:each_slice,2).to_a
|
133
|
-
cells = pairs.map do |v|
|
134
|
-
if v[0]==v[1]
|
135
|
-
{ :txt => v[0], :value => v[0], :cell_mode => "" }
|
136
|
-
else
|
137
|
-
{ :txt => v[0], :value => v[0], :new_value => v[1], :cell_mode => "->" }
|
138
|
-
end
|
139
|
-
end
|
140
|
-
rc = RowChange.new("->",cells)
|
141
|
-
apply_rc(rc,row,keys_in_dbl_cols)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
|
146
|
-
def apply_deletes(sql,all_cols,keys_in_all_cols)
|
147
|
-
@db1.fetch(sql,all_cols) do |row|
|
148
|
-
cells = row.map{|v| { :txt => v, :value => v, :cell_mode => "" }}
|
149
|
-
rc = RowChange.new("---",cells)
|
150
|
-
apply_rc(rc,row,keys_in_all_cols)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def apply_rc(rc,row,keys_in_cols)
|
155
|
-
rc.columns = @rc_columns
|
156
|
-
if @patch.want_context
|
157
|
-
rc.key = keyify(row.values_at(*keys_in_cols))
|
158
|
-
@pending_rcs << rc
|
159
|
-
else
|
160
|
-
@patch.apply_row(rc)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def emit_skip(row)
|
165
|
-
cells = row.map{|v| { :txt => "...", :value => "...", :cell_mode => "" }}
|
166
|
-
rc = RowChange.new("...",cells)
|
167
|
-
rc.columns = @rc_columns
|
168
|
-
@patch.apply_row(rc)
|
169
|
-
end
|
170
|
-
|
171
|
-
# Do the context dance.
|
172
|
-
def apply_with_context(sql,all_cols,keys_in_all_cols)
|
173
|
-
hits = {}
|
174
|
-
@pending_rcs.each do |rc|
|
175
|
-
hits[rc.key] = rc
|
176
|
-
end
|
177
|
-
hist = []
|
178
|
-
n = 2
|
179
|
-
pending = 0
|
180
|
-
skipped = false
|
181
|
-
noted = false
|
182
|
-
last_row = nil
|
183
|
-
@db1.fetch(sql,all_cols + ["__coopy_tag__"]) do |row|
|
184
|
-
tag = row.pop.to_i
|
185
|
-
k = keyify(row.values_at(*keys_in_all_cols))
|
186
|
-
if hits[k]
|
187
|
-
emit_skip(row) if skipped
|
188
|
-
hist.each do |row0|
|
189
|
-
cells = row0.map{|v| { :txt => v, :value => v, :cell_mode => "" }}
|
190
|
-
rc = RowChange.new("",cells)
|
191
|
-
rc.columns = @rc_columns
|
192
|
-
@patch.apply_row(rc)
|
193
|
-
end
|
194
|
-
hist.clear
|
195
|
-
pending = n
|
196
|
-
@patch.apply_row(hits[k])
|
197
|
-
hits.delete(k)
|
198
|
-
skipped = false
|
199
|
-
noted = true
|
200
|
-
elsif tag == 1
|
201
|
-
# ignore redundant row
|
202
|
-
elsif pending>0
|
203
|
-
emit_skip(row) if skipped
|
204
|
-
cells = row.map{|v| { :txt => v, :value => v, :cell_mode => "" }}
|
205
|
-
rc = RowChange.new("",cells)
|
206
|
-
rc.columns = @rc_columns
|
207
|
-
@patch.apply_row(rc)
|
208
|
-
pending = pending-1
|
209
|
-
skipped = false
|
210
|
-
else
|
211
|
-
hist << row
|
212
|
-
if hist.length>n
|
213
|
-
skipped = true
|
214
|
-
last_row = row
|
215
|
-
hist.shift
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
219
|
-
emit_skip(last_row) if skipped and noted
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
data/lib/coopy/sql_wrapper.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
class SqlWrapper
|
2
|
-
def insert(tbl,cols,vals)
|
3
|
-
end
|
4
|
-
|
5
|
-
def delete(tbl,cols,vals)
|
6
|
-
end
|
7
|
-
|
8
|
-
def update(tbl,set_cols,set_vals,cond_cols,cond_vals)
|
9
|
-
end
|
10
|
-
|
11
|
-
def column_names(tbl)
|
12
|
-
[]
|
13
|
-
end
|
14
|
-
|
15
|
-
def primary_key(tbl)
|
16
|
-
[]
|
17
|
-
end
|
18
|
-
|
19
|
-
def except_primary_key(tbl)
|
20
|
-
column_names(tbl)-primary_key(tbl)
|
21
|
-
end
|
22
|
-
|
23
|
-
def fetch(sql)
|
24
|
-
[]
|
25
|
-
end
|
26
|
-
|
27
|
-
def quote_column(c)
|
28
|
-
c.to_s
|
29
|
-
end
|
30
|
-
|
31
|
-
def quote_table(t)
|
32
|
-
t.to_s
|
33
|
-
end
|
34
|
-
end
|
@@ -1,143 +0,0 @@
|
|
1
|
-
require 'coopy/sql_wrapper'
|
2
|
-
|
3
|
-
class SqliteSqlWrapper < SqlWrapper
|
4
|
-
def initialize(db)
|
5
|
-
@db = db
|
6
|
-
@t = nil
|
7
|
-
@qt = nil
|
8
|
-
@pk = nil
|
9
|
-
@info = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def set_primary_key(lst)
|
13
|
-
@pk = lst
|
14
|
-
end
|
15
|
-
|
16
|
-
def sqlite_execute(template,vals)
|
17
|
-
return @db.execute(template,*vals)
|
18
|
-
end
|
19
|
-
|
20
|
-
def get_table_names
|
21
|
-
sqlite_execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",[]).flatten
|
22
|
-
end
|
23
|
-
|
24
|
-
def complete_table(tbl)
|
25
|
-
@t = tbl unless tbl.nil?
|
26
|
-
@t = get_table_names[0] if @t.nil?
|
27
|
-
@t
|
28
|
-
end
|
29
|
-
|
30
|
-
def quote_with_dots(x)
|
31
|
-
return x if x.match(/^[a-zA-Z0-9_]+$/)
|
32
|
-
x.split('.').map{|p| "`#{p}`"}.join('.')
|
33
|
-
end
|
34
|
-
|
35
|
-
def quote_table(tbl)
|
36
|
-
complete_table(tbl)
|
37
|
-
return @t if @t.match(/^[a-zA-Z0-9_]+$/)
|
38
|
-
quote_with_dots(@t)
|
39
|
-
end
|
40
|
-
|
41
|
-
def quote_column(col)
|
42
|
-
return col if col.match(/^[a-zA-Z0-9_]+$/)
|
43
|
-
quote_with_dots(col)
|
44
|
-
end
|
45
|
-
|
46
|
-
def insert(tbl,cols,vals)
|
47
|
-
tbl = quote_table(tbl)
|
48
|
-
template = cols.map{|x| '?'}.join(",")
|
49
|
-
template = "INSERT INTO #{tbl} VALUES(#{template})"
|
50
|
-
sqlite_execute(template,vals)
|
51
|
-
end
|
52
|
-
|
53
|
-
def delete(tbl,cols,vals)
|
54
|
-
tbl = quote_table(tbl)
|
55
|
-
template = cols.map{|c| quote_column(c) + ' IS ?'}.join(" AND ")
|
56
|
-
template = "DELETE FROM #{tbl} WHERE #{template}"
|
57
|
-
sqlite_execute(template,vals)
|
58
|
-
end
|
59
|
-
|
60
|
-
def update(tbl,set_cols,set_vals,cond_cols,cond_vals)
|
61
|
-
tbl = quote_table(tbl)
|
62
|
-
conds = cond_cols.map{|c| quote_column(c) + ' IS ?'}.join(" AND ")
|
63
|
-
sets = set_cols.map{|c| quote_column(c) + ' = ?'}.join(", ")
|
64
|
-
template = "UPDATE #{tbl} SET #{sets} WHERE #{conds}"
|
65
|
-
v = set_vals + cond_vals
|
66
|
-
sqlite_execute(template,v)
|
67
|
-
end
|
68
|
-
|
69
|
-
def transaction(&block)
|
70
|
-
# not yet mapped, not yet used
|
71
|
-
block.call
|
72
|
-
end
|
73
|
-
|
74
|
-
def pragma(tbl,info)
|
75
|
-
if tbl.include? '.'
|
76
|
-
dbname, tbname, *ignore = tbl.split('.')
|
77
|
-
dbname = quote_with_dots(dbname)
|
78
|
-
tbname = quote_with_dots(tbname)
|
79
|
-
query = "PRAGMA #{dbname}.#{info}(#{tbname})"
|
80
|
-
else
|
81
|
-
tbl = quote_with_dots(tbl)
|
82
|
-
query = "PRAGMA #{info}(#{tbl})"
|
83
|
-
end
|
84
|
-
result = sqlite_execute(query,[])
|
85
|
-
result
|
86
|
-
end
|
87
|
-
|
88
|
-
def part(row,n,name)
|
89
|
-
row[n]
|
90
|
-
end
|
91
|
-
|
92
|
-
def columns(tbl)
|
93
|
-
tbl = complete_table(tbl)
|
94
|
-
@info[tbl] = pragma(tbl,"table_info") unless @info.has_key? tbl
|
95
|
-
@info[tbl]
|
96
|
-
end
|
97
|
-
|
98
|
-
def column_names(tbl)
|
99
|
-
columns(tbl).map{|c| part(c,1,"name")}
|
100
|
-
end
|
101
|
-
|
102
|
-
def fetch(sql,names)
|
103
|
-
sqlite_execute(sql,[]).each do |row|
|
104
|
-
yield row
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def primary_key(tbl)
|
109
|
-
return @pk unless @pk.nil?
|
110
|
-
cols = columns(tbl)
|
111
|
-
cols = cols.select{|c| part(c,5,"pk").to_s=="1"}.map{|c| part(c,1,"name")}
|
112
|
-
if cols.length == 0
|
113
|
-
cols = pk_from_unique_index(tbl)
|
114
|
-
end
|
115
|
-
@pk = cols if cols.length>0
|
116
|
-
cols
|
117
|
-
end
|
118
|
-
|
119
|
-
def pk_from_unique_index(tbl)
|
120
|
-
pragma(tbl,"index_list").each do |row|
|
121
|
-
if part(row,2,"unique").to_s == "1"
|
122
|
-
idx = part(row,1,"name")
|
123
|
-
return pragma(idx,"index_info").map{|r| part(r,2,"name")}
|
124
|
-
end
|
125
|
-
end
|
126
|
-
nil
|
127
|
-
end
|
128
|
-
|
129
|
-
# copy the structure of an attached table, along with any indexes
|
130
|
-
def copy_table_structure(rdb,tbl)
|
131
|
-
template = "SELECT sql, type from X.sqlite_master WHERE tbl_name = ? ORDER BY type DESC"
|
132
|
-
lsql = template.gsub('X',"main")
|
133
|
-
rsql = template.gsub('X',quote_with_dots(rdb))
|
134
|
-
args = [quote_with_dots(tbl)]
|
135
|
-
lschema = sqlite_execute(lsql,args)
|
136
|
-
rschema = sqlite_execute(rsql,args)
|
137
|
-
if lschema.length>0
|
138
|
-
return false
|
139
|
-
end
|
140
|
-
rschema.each{ |row| sqlite_execute(row[0],[]) }
|
141
|
-
true
|
142
|
-
end
|
143
|
-
end
|