upsert 0.4.0 → 0.5.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.
@@ -1,118 +1,154 @@
1
1
  require 'digest/md5'
2
2
 
3
3
  class Upsert
4
- # @private
5
4
  class Buffer
6
5
  class PG_Connection < Buffer
6
+ # @private
7
7
  class MergeFunction
8
8
  class << self
9
9
  def execute(buffer, row)
10
- first_try = true
11
- begin
12
- buffer.parent.connection.execute sql(buffer, row)
13
- rescue PG::Error => pg_error
14
- if first_try and pg_error.message =~ /function upsert_(.+) does not exist/
15
- Upsert.logger.info %{[upsert] Function #{"upsert_#{$1}".inspect} went missing, trying to recreate}
16
- first_try = false
17
- @lookup.clear
18
- retry
19
- else
20
- raise pg_error
21
- end
22
- end
23
- end
24
-
25
- def sql(buffer, row)
26
10
  merge_function = lookup buffer, row
27
- %{SELECT #{merge_function.name}(#{merge_function.values_sql(row)})}
11
+ merge_function.execute row
28
12
  end
29
13
 
30
- def unique_key(table_name, selector, columns)
31
- [
14
+ def unique_name(table_name, selector, setter)
15
+ parts = [
16
+ 'upsert',
32
17
  table_name,
33
- selector.join(','),
34
- columns.join(',')
35
- ].join '/'
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
36
26
  end
37
27
 
38
28
  def lookup(buffer, row)
39
29
  @lookup ||= {}
40
- s = row.selector.keys
41
- c = row.columns
42
- @lookup[unique_key(buffer.parent.table_name, s, c)] ||= new(buffer, s, c)
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
43
70
  end
44
71
  end
45
72
 
73
+ MAX_NAME_LENGTH = 63
74
+
46
75
  attr_reader :buffer
47
76
  attr_reader :selector
48
- attr_reader :columns
77
+ attr_reader :setter
49
78
 
50
- def initialize(buffer, selector, columns)
79
+ def initialize(buffer, selector, setter)
51
80
  @buffer = buffer
52
81
  @selector = selector
53
- @columns = columns
82
+ @setter = setter
54
83
  create!
55
84
  end
56
85
 
57
86
  def name
58
- @name ||= "upsert_#{Digest::MD5.hexdigest(unique_key)}"
87
+ @name ||= MergeFunction.unique_name table_name, selector, setter
59
88
  end
60
89
 
61
- def values_sql(row)
62
- ordered_args = columns.map do |k|
63
- row.quoted_value(k) || NULL_WORD
64
- end.join(',')
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
65
111
  end
66
112
 
67
113
  private
68
114
 
69
- def unique_key
70
- @unique_key ||= MergeFunction.unique_key buffer.parent.table_name, selector, columns
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
71
121
  end
72
122
 
73
123
  def connection
74
124
  buffer.parent.connection
75
125
  end
76
126
 
77
- def quoted_table_name
78
- buffer.parent.quoted_table_name
127
+ def table_name
128
+ buffer.parent.table_name
79
129
  end
80
130
 
81
- ColumnDefinition = Struct.new(:quoted_name, :quoted_input_name, :sql_type, :default)
82
-
83
- # activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
84
- def get_column_definitions
85
- res = connection.execute <<-EOS
86
- SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS sql_type, d.adsrc AS default
87
- FROM pg_attribute a LEFT JOIN pg_attrdef d
88
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
89
- WHERE a.attrelid = '#{quoted_table_name}'::regclass
90
- AND a.attnum > 0 AND NOT a.attisdropped
91
- EOS
92
- unsorted = res.select do |row|
93
- columns.include? row['name']
94
- end.inject({}) do |memo, row|
95
- k = row['name']
96
- memo[k] = ColumnDefinition.new connection.quote_ident(k), connection.quote_ident("#{k}_input"), row['sql_type'], row['default']
97
- memo
98
- end
99
- columns.map do |k|
100
- unsorted[k]
101
- end
131
+ def quoted_table_name
132
+ buffer.parent.quoted_table_name
102
133
  end
103
134
 
104
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
105
137
  def create!
106
- Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{buffer.parent.table_name.inspect} for selector #{selector.map(&:inspect).join(', ')} and columns #{columns.map(&:inspect).join(', ')}"
107
- column_definitions = get_column_definitions
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) }
108
142
  connection.execute <<-EOS
109
- CREATE OR REPLACE FUNCTION #{name}(#{column_definitions.map { |c| "#{c.quoted_input_name} #{c.sql_type} DEFAULT #{c.default || 'NULL'}" }.join(',') }) RETURNS VOID AS
143
+ CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
110
144
  $$
145
+ DECLARE
146
+ first_try INTEGER := 1;
111
147
  BEGIN
112
148
  LOOP
113
149
  -- first try to update the key
114
- UPDATE #{quoted_table_name} SET #{column_definitions.map { |c| "#{c.quoted_name} = #{c.quoted_input_name}" }.join(',')}
115
- WHERE #{selector.map { |k| "#{connection.quote_ident(k)} = #{connection.quote_ident([k,'input'].join('_'))}" }.join(' AND ') };
150
+ UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')}
151
+ WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
116
152
  IF found THEN
117
153
  RETURN;
118
154
  END IF;
@@ -120,9 +156,15 @@ BEGIN
120
156
  -- if someone else inserts the same key concurrently,
121
157
  -- we could get a unique-key failure
122
158
  BEGIN
123
- INSERT INTO #{quoted_table_name}(#{column_definitions.map { |c| c.quoted_name }.join(',')}) VALUES (#{column_definitions.map { |c| c.quoted_input_name }.join(',')});
159
+ INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:quoted_setter_name).join(', ')});
124
160
  RETURN;
125
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;
126
168
  -- Do nothing, and loop to try the UPDATE again.
127
169
  END;
128
170
  END LOOP;
@@ -131,7 +173,6 @@ $$
131
173
  LANGUAGE plpgsql;
132
174
  EOS
133
175
  end
134
-
135
176
  end
136
177
  end
137
178
  end
@@ -5,8 +5,16 @@ class Upsert
5
5
  def ready
6
6
  return if rows.empty?
7
7
  row = rows.shift
8
- c = parent.connection
9
- c.execute %{INSERT OR IGNORE INTO #{parent.quoted_table_name} (#{row.columns_sql}) VALUES (#{row.values_sql}); UPDATE #{parent.quoted_table_name} SET #{row.set_sql} WHERE #{row.where_sql}}
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))
10
18
  end
11
19
  end
12
20
  end
@@ -0,0 +1,9 @@
1
+ require 'upsert/cell/mysql2_client'
2
+ require 'upsert/cell/pg_connection'
3
+ require 'upsert/cell/sqlite3_database'
4
+
5
+ class Upsert
6
+ # @private
7
+ class Cell
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ class Upsert
2
+ class Cell
3
+ # @private
4
+ class Mysql2_Client < Cell
5
+ attr_reader :name
6
+ attr_reader :value
7
+ attr_reader :quoted_value
8
+
9
+ def initialize(connection, name, value)
10
+ @name = name
11
+ @value = value
12
+ @quoted_value = connection.quote_value value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ class Upsert
2
+ class Cell
3
+ # @private
4
+ class PG_Connection < Cell
5
+ attr_reader :name
6
+ attr_reader :value
7
+ attr_reader :quoted_name
8
+
9
+ def initialize(connection, name, value)
10
+ @name = name
11
+ @value = value
12
+ @quoted_name = connection.quote_ident name
13
+ end
14
+
15
+ def bind_value
16
+ return @bind_value if defined?(@bind_value)
17
+ @bind_value = case value
18
+ when Upsert::Binary
19
+ { :value => value.value, :format => 1 }
20
+ when Time, DateTime
21
+ [value.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, value.usec)].join('.')
22
+ else
23
+ value
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ class Upsert
2
+ class Cell
3
+ # @private
4
+ class SQLite3_Database < Cell
5
+ attr_reader :name
6
+ attr_reader :value
7
+ attr_reader :quoted_name
8
+
9
+ def initialize(connection, name, value)
10
+ @name = name
11
+ @value = value
12
+ @quoted_name = connection.quote_ident name
13
+ end
14
+
15
+ def bind_value
16
+ return @bind_value if defined?(@bind_value)
17
+ @bind_value = case value
18
+ when Upsert::Binary
19
+ SQLite3::Blob.new value.value
20
+ when BigDecimal
21
+ value.to_s('F')
22
+ when TrueClass
23
+ 't'
24
+ when FalseClass
25
+ 'f'
26
+ when Time, DateTime
27
+ [value.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, value.usec)].join('.')
28
+ when Date
29
+ value.strftime ISO8601_DATE
30
+ else
31
+ value
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -18,7 +18,7 @@ class Upsert
18
18
  when NilClass
19
19
  NULL_WORD
20
20
  when Upsert::Binary
21
- quote_binary v # must be defined by base
21
+ quote_binary v.value # must be defined by base
22
22
  when String
23
23
  quote_string v # must be defined by base
24
24
  when TrueClass, FalseClass
@@ -2,38 +2,19 @@ class Upsert
2
2
  class Connection
3
3
  # @private
4
4
  class PG_Connection < Connection
5
- def execute(sql)
6
- Upsert.logger.debug { %{[upsert] #{sql}} }
7
- raw_connection.exec sql
8
- end
9
-
10
- def quote_string(v)
11
- SINGLE_QUOTE + raw_connection.escape_string(v) + SINGLE_QUOTE
12
- end
13
-
14
- def quote_binary(v)
15
- E_AND_SINGLE_QUOTE + raw_connection.escape_bytea(v) + SINGLE_QUOTE
16
- end
17
-
18
- def quote_time(v)
19
- quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
20
- end
21
-
22
- def quote_big_decimal(v)
23
- v.to_s('F')
24
- end
25
-
26
- def quote_boolean(v)
27
- v ? 'TRUE' : 'FALSE'
5
+ def execute(sql, params = nil)
6
+ if params
7
+ Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
8
+ raw_connection.exec sql, params
9
+ else
10
+ Upsert.logger.debug { %{[upsert] #{sql}} }
11
+ raw_connection.exec sql
12
+ end
28
13
  end
29
14
 
30
15
  def quote_ident(k)
31
16
  raw_connection.quote_ident k.to_s
32
17
  end
33
18
  end
34
-
35
- # @private
36
- # backwards compatibility - https://github.com/seamusabshere/upsert/issues/2
37
- PGconn = PG_Connection
38
19
  end
39
20
  end
@@ -2,35 +2,19 @@ class Upsert
2
2
  class Connection
3
3
  # @private
4
4
  class SQLite3_Database < Connection
5
- def execute(sql)
6
- Upsert.logger.debug { %{[upsert] #{sql}} }
7
- raw_connection.execute_batch sql
8
- end
9
-
10
- def quote_string(v)
11
- SINGLE_QUOTE + SQLite3::Database.quote(v) + SINGLE_QUOTE
12
- end
13
-
14
- def quote_binary(v)
15
- X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
16
- end
17
-
18
- def quote_time(v)
19
- quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
5
+ def execute(sql, params = nil)
6
+ if params
7
+ Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
8
+ raw_connection.execute sql, params
9
+ else
10
+ Upsert.logger.debug { %{[upsert] #{sql}} }
11
+ raw_connection.execute sql
12
+ end
20
13
  end
21
14
 
22
15
  def quote_ident(k)
23
16
  DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
24
17
  end
25
-
26
- def quote_boolean(v)
27
- s = v ? 't' : 'f'
28
- quote_string s
29
- end
30
-
31
- def quote_big_decimal(v)
32
- v.to_f
33
- end
34
18
  end
35
19
  end
36
20
  end