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