upsert 0.5.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/CHANGELOG +29 -0
  2. data/README.md +165 -105
  3. data/lib/upsert.rb +32 -17
  4. data/lib/upsert/cell.rb +0 -4
  5. data/lib/upsert/cell/{mysql2_client.rb → Mysql2_Client.rb} +0 -0
  6. data/lib/upsert/cell/{pg_connection.rb → PG_Connection.rb} +0 -0
  7. data/lib/upsert/cell/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
  8. data/lib/upsert/column_definition.rb +43 -0
  9. data/lib/upsert/column_definition/Mysql2_Client.rb +24 -0
  10. data/lib/upsert/column_definition/PG_Connection.rb +24 -0
  11. data/lib/upsert/column_definition/SQLite3_Database.rb +7 -0
  12. data/lib/upsert/connection.rb +3 -7
  13. data/lib/upsert/connection/{mysql2_client.rb → Mysql2_Client.rb} +0 -0
  14. data/lib/upsert/connection/{pg_connection.rb → PG_Connection.rb} +0 -0
  15. data/lib/upsert/connection/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
  16. data/lib/upsert/merge_function.rb +72 -0
  17. data/lib/upsert/merge_function/Mysql2_Client.rb +89 -0
  18. data/lib/upsert/merge_function/PG_Connection.rb +114 -0
  19. data/lib/upsert/merge_function/SQLite3_Database.rb +29 -0
  20. data/lib/upsert/row.rb +3 -7
  21. data/lib/upsert/row/{mysql2_client.rb → Mysql2_Client.rb} +1 -1
  22. data/lib/upsert/row/{pg_connection.rb → PG_Connection.rb} +0 -0
  23. data/lib/upsert/row/{sqlite3_database.rb → SQLite3_Database.rb} +0 -0
  24. data/lib/upsert/version.rb +1 -1
  25. data/spec/correctness_spec.rb +15 -1
  26. data/spec/database_functions_spec.rb +32 -26
  27. data/spec/logger_spec.rb +8 -8
  28. data/spec/spec_helper.rb +11 -5
  29. data/spec/type_safety_spec.rb +11 -0
  30. data/upsert.gemspec +4 -2
  31. metadata +41 -22
  32. data/lib/upsert/buffer.rb +0 -36
  33. data/lib/upsert/buffer/mysql2_client.rb +0 -80
  34. data/lib/upsert/buffer/pg_connection.rb +0 -19
  35. data/lib/upsert/buffer/pg_connection/column_definition.rb +0 -59
  36. data/lib/upsert/buffer/pg_connection/merge_function.rb +0 -179
  37. 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