upsert 0.5.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +29 -0
- data/README.md +165 -105
- data/lib/upsert.rb +32 -17
- data/lib/upsert/cell.rb +0 -4
- data/lib/upsert/cell/{mysql2_client.rb → Mysql2_Client.rb} +0 -0
- data/lib/upsert/cell/{pg_connection.rb → PG_Connection.rb} +0 -0
- data/lib/upsert/cell/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
- data/lib/upsert/column_definition.rb +43 -0
- data/lib/upsert/column_definition/Mysql2_Client.rb +24 -0
- data/lib/upsert/column_definition/PG_Connection.rb +24 -0
- data/lib/upsert/column_definition/SQLite3_Database.rb +7 -0
- data/lib/upsert/connection.rb +3 -7
- data/lib/upsert/connection/{mysql2_client.rb → Mysql2_Client.rb} +0 -0
- data/lib/upsert/connection/{pg_connection.rb → PG_Connection.rb} +0 -0
- data/lib/upsert/connection/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
- data/lib/upsert/merge_function.rb +72 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +89 -0
- data/lib/upsert/merge_function/PG_Connection.rb +114 -0
- data/lib/upsert/merge_function/SQLite3_Database.rb +29 -0
- data/lib/upsert/row.rb +3 -7
- data/lib/upsert/row/{mysql2_client.rb → Mysql2_Client.rb} +1 -1
- data/lib/upsert/row/{pg_connection.rb → PG_Connection.rb} +0 -0
- data/lib/upsert/row/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
- data/lib/upsert/version.rb +1 -1
- data/spec/correctness_spec.rb +15 -1
- data/spec/database_functions_spec.rb +32 -26
- data/spec/logger_spec.rb +8 -8
- data/spec/spec_helper.rb +11 -5
- data/spec/type_safety_spec.rb +11 -0
- data/upsert.gemspec +4 -2
- metadata +41 -22
- data/lib/upsert/buffer.rb +0 -36
- data/lib/upsert/buffer/mysql2_client.rb +0 -80
- data/lib/upsert/buffer/pg_connection.rb +0 -19
- data/lib/upsert/buffer/pg_connection/column_definition.rb +0 -59
- data/lib/upsert/buffer/pg_connection/merge_function.rb +0 -179
- data/lib/upsert/buffer/sqlite3_database.rb +0 -21
@@ -1,19 +0,0 @@
|
|
1
|
-
require 'upsert/buffer/pg_connection/column_definition'
|
2
|
-
require 'upsert/buffer/pg_connection/merge_function'
|
3
|
-
|
4
|
-
class Upsert
|
5
|
-
class Buffer
|
6
|
-
# @private
|
7
|
-
class PG_Connection < Buffer
|
8
|
-
def ready
|
9
|
-
return if rows.empty?
|
10
|
-
row = rows.shift
|
11
|
-
MergeFunction.execute self, row
|
12
|
-
end
|
13
|
-
|
14
|
-
def clear_database_functions
|
15
|
-
MergeFunction.clear self
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
class Upsert
|
2
|
-
class Buffer
|
3
|
-
class PG_Connection < Buffer
|
4
|
-
# @private
|
5
|
-
class ColumnDefinition
|
6
|
-
class << self
|
7
|
-
# activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
|
8
|
-
def all(buffer, table_name)
|
9
|
-
connection = buffer.parent.connection
|
10
|
-
res = connection.execute <<-EOS
|
11
|
-
SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS sql_type, d.adsrc AS default
|
12
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
13
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
14
|
-
WHERE a.attrelid = '#{connection.quote_ident(table_name)}'::regclass
|
15
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
16
|
-
EOS
|
17
|
-
res.map do |row|
|
18
|
-
new connection, row['name'], row['sql_type'], row['default']
|
19
|
-
end.sort_by do |cd|
|
20
|
-
cd.name
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
attr_reader :name
|
26
|
-
attr_reader :sql_type
|
27
|
-
attr_reader :default
|
28
|
-
attr_reader :quoted_name
|
29
|
-
attr_reader :quoted_selector_name
|
30
|
-
attr_reader :quoted_setter_name
|
31
|
-
|
32
|
-
def initialize(connection, name, sql_type, default)
|
33
|
-
@name = name
|
34
|
-
@sql_type = sql_type
|
35
|
-
@default = default
|
36
|
-
@quoted_name = connection.quote_ident name
|
37
|
-
@quoted_selector_name = connection.quote_ident "#{name}_selector"
|
38
|
-
@quoted_setter_name = connection.quote_ident "#{name}_setter"
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_selector_arg
|
42
|
-
"#{quoted_selector_name} #{sql_type}"
|
43
|
-
end
|
44
|
-
|
45
|
-
def to_setter_arg
|
46
|
-
"#{quoted_setter_name} #{sql_type}"
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_setter
|
50
|
-
"#{quoted_name} = #{quoted_setter_name}"
|
51
|
-
end
|
52
|
-
|
53
|
-
def to_selector
|
54
|
-
"#{quoted_name} = #{quoted_selector_name}"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
@@ -1,179 +0,0 @@
|
|
1
|
-
require 'digest/md5'
|
2
|
-
|
3
|
-
class Upsert
|
4
|
-
class Buffer
|
5
|
-
class PG_Connection < Buffer
|
6
|
-
# @private
|
7
|
-
class MergeFunction
|
8
|
-
class << self
|
9
|
-
def execute(buffer, row)
|
10
|
-
merge_function = lookup buffer, row
|
11
|
-
merge_function.execute row
|
12
|
-
end
|
13
|
-
|
14
|
-
def unique_name(table_name, selector, setter)
|
15
|
-
parts = [
|
16
|
-
'upsert',
|
17
|
-
table_name,
|
18
|
-
'SEL',
|
19
|
-
selector.join('_A_'),
|
20
|
-
'SET',
|
21
|
-
setter.join('_A_')
|
22
|
-
].join('_')
|
23
|
-
# maybe i should md5 instead
|
24
|
-
crc32 = Zlib.crc32(parts).to_s
|
25
|
-
[ parts.first(MAX_NAME_LENGTH-11), crc32 ].join
|
26
|
-
end
|
27
|
-
|
28
|
-
def lookup(buffer, row)
|
29
|
-
@lookup ||= {}
|
30
|
-
selector = row.selector.keys
|
31
|
-
setter = row.setter.keys
|
32
|
-
key = [buffer.parent.table_name, selector, setter]
|
33
|
-
@lookup[key] ||= new(buffer, selector, setter)
|
34
|
-
end
|
35
|
-
|
36
|
-
def clear(buffer)
|
37
|
-
connection = buffer.parent.connection
|
38
|
-
# http://stackoverflow.com/questions/7622908/postgresql-drop-function-without-knowing-the-number-type-of-parameters
|
39
|
-
connection.execute <<-EOS
|
40
|
-
CREATE OR REPLACE FUNCTION pg_temp.upsert_delfunc(text)
|
41
|
-
RETURNS void AS
|
42
|
-
$BODY$
|
43
|
-
DECLARE
|
44
|
-
_sql text;
|
45
|
-
BEGIN
|
46
|
-
|
47
|
-
FOR _sql IN
|
48
|
-
SELECT 'DROP FUNCTION ' || quote_ident(n.nspname)
|
49
|
-
|| '.' || quote_ident(p.proname)
|
50
|
-
|| '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');'
|
51
|
-
FROM pg_catalog.pg_proc p
|
52
|
-
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
53
|
-
WHERE p.proname = $1
|
54
|
-
AND pg_catalog.pg_function_is_visible(p.oid) -- you may or may not want this
|
55
|
-
LOOP
|
56
|
-
EXECUTE _sql;
|
57
|
-
END LOOP;
|
58
|
-
|
59
|
-
END;
|
60
|
-
$BODY$
|
61
|
-
LANGUAGE plpgsql;
|
62
|
-
EOS
|
63
|
-
res = connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE 'upsert_%'})
|
64
|
-
res.each do |row|
|
65
|
-
k = row['proname']
|
66
|
-
next if k == 'upsert_delfunc'
|
67
|
-
Upsert.logger.info %{[upsert] Dropping function #{k.inspect}}
|
68
|
-
connection.execute %{SELECT pg_temp.upsert_delfunc('#{k}')}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
MAX_NAME_LENGTH = 63
|
74
|
-
|
75
|
-
attr_reader :buffer
|
76
|
-
attr_reader :selector
|
77
|
-
attr_reader :setter
|
78
|
-
|
79
|
-
def initialize(buffer, selector, setter)
|
80
|
-
@buffer = buffer
|
81
|
-
@selector = selector
|
82
|
-
@setter = setter
|
83
|
-
create!
|
84
|
-
end
|
85
|
-
|
86
|
-
def name
|
87
|
-
@name ||= MergeFunction.unique_name table_name, selector, setter
|
88
|
-
end
|
89
|
-
|
90
|
-
def execute(row)
|
91
|
-
first_try = true
|
92
|
-
bind_selector_values = row.selector.values.map(&:bind_value)
|
93
|
-
bind_setter_values = row.setter.values.map(&:bind_value)
|
94
|
-
begin
|
95
|
-
connection.execute sql, (bind_selector_values + bind_setter_values)
|
96
|
-
rescue PG::Error => pg_error
|
97
|
-
if pg_error.message =~ /function #{name}.* does not exist/i
|
98
|
-
if first_try
|
99
|
-
Upsert.logger.info %{[upsert] Function #{name.inspect} went missing, trying to recreate}
|
100
|
-
first_try = false
|
101
|
-
create!
|
102
|
-
retry
|
103
|
-
else
|
104
|
-
Upsert.logger.info %{[upsert] Failed to create function #{name.inspect} for some reason}
|
105
|
-
raise pg_error
|
106
|
-
end
|
107
|
-
else
|
108
|
-
raise pg_error
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
private
|
114
|
-
|
115
|
-
def sql
|
116
|
-
@sql ||= begin
|
117
|
-
bind_params = []
|
118
|
-
1.upto(selector.length + setter.length) { |i| bind_params << "$#{i}" }
|
119
|
-
%{SELECT #{name}(#{bind_params.join(', ')})}
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def connection
|
124
|
-
buffer.parent.connection
|
125
|
-
end
|
126
|
-
|
127
|
-
def table_name
|
128
|
-
buffer.parent.table_name
|
129
|
-
end
|
130
|
-
|
131
|
-
def quoted_table_name
|
132
|
-
buffer.parent.quoted_table_name
|
133
|
-
end
|
134
|
-
|
135
|
-
# the "canonical example" from http://www.postgresql.org/docs/9.1/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
|
136
|
-
# differentiate between selector and setter
|
137
|
-
def create!
|
138
|
-
Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector.map(&:inspect).join(', ')} and setter #{setter.map(&:inspect).join(', ')}"
|
139
|
-
column_definitions = ColumnDefinition.all buffer, table_name
|
140
|
-
selector_column_definitions = column_definitions.select { |cd| selector.include?(cd.name) }
|
141
|
-
setter_column_definitions = column_definitions.select { |cd| setter.include?(cd.name) }
|
142
|
-
connection.execute <<-EOS
|
143
|
-
CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
|
144
|
-
$$
|
145
|
-
DECLARE
|
146
|
-
first_try INTEGER := 1;
|
147
|
-
BEGIN
|
148
|
-
LOOP
|
149
|
-
-- first try to update the key
|
150
|
-
UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')}
|
151
|
-
WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
|
152
|
-
IF found THEN
|
153
|
-
RETURN;
|
154
|
-
END IF;
|
155
|
-
-- not there, so try to insert the key
|
156
|
-
-- if someone else inserts the same key concurrently,
|
157
|
-
-- we could get a unique-key failure
|
158
|
-
BEGIN
|
159
|
-
INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:quoted_setter_name).join(', ')});
|
160
|
-
RETURN;
|
161
|
-
EXCEPTION WHEN unique_violation THEN
|
162
|
-
-- seamusabshere 9/20/12 only retry once
|
163
|
-
IF (first_try = 1) THEN
|
164
|
-
first_try := 0;
|
165
|
-
ELSE
|
166
|
-
RETURN;
|
167
|
-
END IF;
|
168
|
-
-- Do nothing, and loop to try the UPDATE again.
|
169
|
-
END;
|
170
|
-
END LOOP;
|
171
|
-
END;
|
172
|
-
$$
|
173
|
-
LANGUAGE plpgsql;
|
174
|
-
EOS
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
class Upsert
|
2
|
-
class Buffer
|
3
|
-
# @private
|
4
|
-
class SQLite3_Database < Buffer
|
5
|
-
def ready
|
6
|
-
return if rows.empty?
|
7
|
-
row = rows.shift
|
8
|
-
connection = parent.connection
|
9
|
-
bind_setter_values = row.setter.values.map(&:bind_value)
|
10
|
-
quoted_setter_names = row.setter.values.map(&:quoted_name)
|
11
|
-
quoted_selector_names = row.selector.values.map(&:quoted_name)
|
12
|
-
|
13
|
-
insert_or_ignore_sql = %{INSERT OR IGNORE INTO #{parent.quoted_table_name} (#{quoted_setter_names.join(',')}) VALUES (#{Array.new(bind_setter_values.length, '?').join(',')})}
|
14
|
-
connection.execute insert_or_ignore_sql, bind_setter_values
|
15
|
-
|
16
|
-
update_sql = %{UPDATE #{parent.quoted_table_name} SET #{quoted_setter_names.map { |qk| "#{qk}=?" }.join(',')} WHERE #{quoted_selector_names.map { |qk| "#{qk}=?" }.join(' AND ')}}
|
17
|
-
connection.execute update_sql, (bind_setter_values + row.selector.values.map(&:bind_value))
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|