upsert 0.5.0 → 1.0.2
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/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
|