activerecord-copy 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b4dcb1725736ffe1229685b13945ebf201578f5
4
- data.tar.gz: d0f4faf9fb27cf0e9712853caddad91140071018
3
+ metadata.gz: 050aa5f637d6f6d6a155e4d9f90c30062d62398e
4
+ data.tar.gz: b6c02d2fc46f8bfd2021b60ece7f42a19d2822ab
5
5
  SHA512:
6
- metadata.gz: f15a8391484bd16250b6b4ff6285df87c5c682817bed3a8efa90add86993edd98ae25192532415edd17e3f573804f1b5f65b8c6364a160b60d76e5a6b4d928a5
7
- data.tar.gz: 9a40ea85b320b12c9648bd00b5938e9f98f930b88d30d3621bb02e05cee3ad7807c21ccb9fd28ba18a88d7ddcdcab40d54d236411458c313e5e9ef5dd8bbf0db
6
+ metadata.gz: 7ffb2c84b4fa363f904dfd82cccb99116d96227b72b5cd871a6c647057b6fa229ad7f1c2ff3645e3ad1a124c7c83ce25486a0203837fab62a8b02745d186f833
7
+ data.tar.gz: 851ad2c3cfc6a986dbc184e2672b369cf83d3dadc8b33ee4709ff58f1451c8c86d82be1bd7f3d9ba8c5a8dd66c2f8cf0ee3897f2e422f6b42d0bf9028713eb82
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 2018-05-24
4
+
5
+ * Add support for range data types
6
+ * Fix bugs in NUMERIC data type encoding
7
+
8
+
3
9
  ## 1.0.1 2017-07-22
4
10
 
5
11
  * Explicitly include ActiveRecord to ensure copy_from_client gets defined
@@ -1,7 +1,23 @@
1
1
  require 'tempfile'
2
2
  require 'stringio'
3
+ require 'ipaddr'
3
4
 
4
5
  module ActiveRecordCopy
6
+ class IntermediateBuffer
7
+ attr_reader :bytes
8
+ def initialize
9
+ @bytes = ''
10
+ end
11
+
12
+ def write(b)
13
+ @bytes += b
14
+ end
15
+
16
+ def size
17
+ @bytes.size
18
+ end
19
+ end
20
+
5
21
  class EncodeForCopy
6
22
  def initialize(options = {})
7
23
  @options = options
@@ -62,19 +78,9 @@ module ActiveRecordCopy
62
78
  io.write(buf)
63
79
  end
64
80
 
65
- def encode_field(io, field, index, depth = 0)
66
- # Nil is an exception in that any kind of field type can have a nil value transmitted
67
- if field.nil?
68
- io.write([-1].pack(PACKED_UINT_32))
69
- return
70
- end
71
-
72
- if field.is_a?(Array) && ![:json, :jsonb].include?(@column_types[index])
73
- encode_array(io, field, index)
74
- return
75
- end
76
-
77
- case @column_types[index]
81
+ # Primitive types that can also appear in ranges/arrays/etc
82
+ def write_simple_field(io, field, type)
83
+ case type
78
84
  when :bigint
79
85
  buf = [field.to_i].pack(PACKED_UINT_64)
80
86
  write_field(io, buf)
@@ -89,6 +95,32 @@ module ActiveRecordCopy
89
95
  when :float
90
96
  buf = [field].pack(PACKED_FLOAT_64)
91
97
  write_field(io, buf)
98
+ when :timestamp, :timestamptz
99
+ buf = [(field.tv_sec * 1_000_000 + field.tv_usec - POSTGRES_EPOCH_TIME).to_i].pack(PACKED_UINT_64)
100
+ write_field(io, buf)
101
+ when :date
102
+ buf = [(field - Date.new(2000, 1, 1)).to_i].pack(PACKED_UINT_32)
103
+ write_field(io, buf)
104
+ else
105
+ raise Exception, "Unsupported simple type: #{type}"
106
+ end
107
+ end
108
+
109
+ def encode_field(io, field, index, depth = 0)
110
+ # Nil is an exception in that any kind of field type can have a nil value transmitted
111
+ if field.nil?
112
+ io.write([-1].pack(PACKED_UINT_32))
113
+ return
114
+ end
115
+
116
+ if field.is_a?(Array) && ![:json, :jsonb].include?(@column_types[index])
117
+ encode_array(io, field, index)
118
+ return
119
+ end
120
+
121
+ case @column_types[index]
122
+ when :bigint, :integer, :smallint, :numeric, :float
123
+ write_simple_field(io, field, @column_types[index])
92
124
  when :uuid
93
125
  buf = [field.delete('-')].pack(PACKED_HEX_STRING)
94
126
  write_field(io, buf)
@@ -101,6 +133,8 @@ module ActiveRecordCopy
101
133
  write_field(io, buf)
102
134
  when :jsonb
103
135
  encode_jsonb(io, field)
136
+ when :int4range, :int8range, :numrange, :tsrange, :tstzrange, :daterange
137
+ encode_range(io, field, @column_types[index])
104
138
  else
105
139
  encode_based_on_input(io, field, index, depth)
106
140
  end
@@ -135,13 +169,26 @@ module ActiveRecordCopy
135
169
  io.write([hash_io.pos].pack(PACKED_UINT_32)) # size of hstore data
136
170
  io.write(hash_io.string)
137
171
  when Time
138
- buf = [(field.tv_sec * 1_000_000 + field.tv_usec - POSTGRES_EPOCH_TIME).to_i].pack(PACKED_UINT_64)
139
- write_field(io, buf)
172
+ write_simple_field(io, field, :timestamp)
140
173
  when Date
141
- buf = [(field - Date.new(2000, 1, 1)).to_i].pack(PACKED_UINT_32)
142
- write_field(io, buf)
174
+ write_simple_field(io, field, :date)
143
175
  when IPAddr
144
176
  encode_ip_addr(io, field)
177
+ when Range
178
+ range_type = case field.begin
179
+ when Integer
180
+ :int4range
181
+ when Float
182
+ :numrange
183
+ when Time
184
+ :tstzrange
185
+ when Date
186
+ :daterange
187
+ else
188
+ raise Exception, "Unsupported range input type #{field.begin.class.name} for index #{index}"
189
+ end
190
+
191
+ encode_range(io, field, range_type)
145
192
  else
146
193
  raise Exception, "Unsupported Format: #{field.class.name}"
147
194
  end
@@ -220,6 +267,49 @@ module ActiveRecordCopy
220
267
  io.write(ip_addr.hton)
221
268
  end
222
269
 
270
+ # From the Postgres source:
271
+ # Binary representation: The first byte is the flags, then the lower bound
272
+ # (if present), then the upper bound (if present). Each bound is represented
273
+ # by a 4-byte length header and the binary representation of that bound (as
274
+ # returned by a call to the send function for the subtype).
275
+ RANGE_LB_INC = 0x02 # lower bound is inclusive
276
+ RANGE_UB_INC = 0x04 # upper bound is inclusive
277
+ RANGE_LB_INF = 0x08 # lower bound is -infinity
278
+ RANGE_UB_INF = 0x10 # upper bound is +infinity
279
+ def encode_range(io, range, range_type)
280
+ field_data_type = case range_type
281
+ when :int4range
282
+ :integer
283
+ when :int8range
284
+ :bigint
285
+ when :numrange
286
+ :numeric
287
+ when :tsrange
288
+ :timestamp
289
+ when :tstzrange
290
+ :timestamptz
291
+ when :daterange
292
+ :date
293
+ else
294
+ raise Exception, "Unsupported range type: #{range_type}"
295
+ end
296
+ flags = 0
297
+ flags |= RANGE_LB_INC # Ruby ranges always include the lower bound
298
+ flags |= RANGE_UB_INC unless range.exclude_end?
299
+ flags |= RANGE_LB_INF if range.begin.respond_to?(:infinite?) && range.begin.infinite?
300
+ flags |= RANGE_UB_INF if range.end.respond_to?(:infinite?) && range.end.infinite?
301
+ tmp_io = IntermediateBuffer.new
302
+ tmp_io.write([flags].pack(PACKED_UINT_8))
303
+ if range.begin && (!range.begin.respond_to?(:infinite?) || !range.begin.infinite?)
304
+ write_simple_field(tmp_io, range.begin, field_data_type)
305
+ end
306
+ if range.end && (!range.end.respond_to?(:infinite?) || !range.end.infinite?)
307
+ write_simple_field(tmp_io, range.end, field_data_type)
308
+ end
309
+ io.write([tmp_io.size].pack(PACKED_UINT_32))
310
+ io.write(tmp_io.bytes)
311
+ end
312
+
223
313
  def encode_jsonb(io, field)
224
314
  buf = field.to_json.encode(UTF_8_ENCODING)
225
315
  io.write([1 + buf.bytesize].pack(PACKED_UINT_32))
@@ -227,7 +317,19 @@ module ActiveRecordCopy
227
317
  io.write(buf)
228
318
  end
229
319
 
230
- NUMERIC_DEC_DIGITS = 4 # NBASE=10000
320
+ NUMERIC_NBASE = 10000
321
+ def base10_to_base10000(intval)
322
+ digits = []
323
+ loop do
324
+ newintval = intval / NUMERIC_NBASE
325
+ digits << intval - newintval * NUMERIC_NBASE
326
+ intval = newintval
327
+ break if intval == 0
328
+ end
329
+ digits
330
+ end
331
+
332
+ NUMERIC_DEC_DIGITS = 4
231
333
  def encode_numeric(io, field)
232
334
  float_str = field.to_s
233
335
  digits_base10 = float_str.scan(/\d/).map(&:to_i)
@@ -235,8 +337,11 @@ module ActiveRecordCopy
235
337
  sign = field < 0.0 ? 0x4000 : 0
236
338
  dscale = digits_base10.size - weight_base10
237
339
 
238
- digits_before_decpoint = digits_base10[0..weight_base10].reverse.each_slice(NUMERIC_DEC_DIGITS).map { |d| d.reverse.map(&:to_s).join.to_i }.reverse
239
- digits_after_decpoint = digits_base10[weight_base10..-1].each_slice(NUMERIC_DEC_DIGITS).map { |d| d.map(&:to_s).join.to_i }
340
+ int_part, frac_part = float_str.split('.')
341
+ frac_part += '0' * (NUMERIC_DEC_DIGITS - frac_part.size % NUMERIC_DEC_DIGITS) # Add trailing zeroes so digit calculations are correct
342
+
343
+ digits_before_decpoint = base10_to_base10000(int_part.to_i)
344
+ digits_after_decpoint = base10_to_base10000(frac_part.to_i).reverse
240
345
 
241
346
  weight = digits_before_decpoint.size - 1
242
347
  digits = digits_before_decpoint + digits_after_decpoint
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordCopy
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
@@ -412,4 +412,27 @@ describe 'generating data' do
412
412
  # File.open('spec/fixtures/output.dat', 'w:ASCII-8BIT') {|out| out.write(str) }
413
413
  expect(str).to eq existing_data
414
414
  end
415
+
416
+ # CREATE TABLE test(i4r int4range, i8r int8range, nr numrange, tr tsrange, tzr tstzrange, dr daterange);
417
+ # INSERT INTO test VALUES ('[12, 14)', '[223372033854775802, 223372033854775810)', '[12.5,13.88211]', '[2010-01-01 15:20, 2010-01-01 15:30)', '[2018-05-24 00:00:00+00,)', '[2018-05-24,)');
418
+ # \copy test TO range_test.dat WITH (FORMAT BINARY);
419
+ it 'encodes range data correctly' do
420
+ encoder = ActiveRecordCopy::EncodeForCopy.new(column_types: { 0 => :int4range, 1 => :int8range, 2 => :numrange, 3 => :tsrange, 4 => :tstzrange, 5 => :daterange })
421
+ encoder.add([
422
+ 12...14,
423
+ 223372033854775802...223372033854775810,
424
+ 12.5..13.88211,
425
+ Time.parse('2010-01-01 15:20+00')...Time.parse('2010-01-01 15:30+00'),
426
+ Time.parse('2018-05-24 00:00:00+00')...Float::INFINITY,
427
+ Date.parse('2018-05-24')...Float::INFINITY
428
+ ])
429
+ encoder.close
430
+ io = encoder.get_io
431
+ existing_data = filedata('range_test.dat')
432
+ str = io.read
433
+ expect(io.class.name).to eq 'StringIO'
434
+ str.force_encoding('ASCII-8BIT')
435
+ #File.open('spec/fixtures/output.dat', 'w:ASCII-8BIT') {|out| out.write(str) }
436
+ expect(str).to eq existing_data
437
+ end
415
438
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-copy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukas Fittl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-23 00:00:00.000000000 Z
11
+ date: 2018-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -103,6 +103,7 @@ files:
103
103
  - spec/fixtures/just_an_array2.dat
104
104
  - spec/fixtures/multiline_hstore.dat
105
105
  - spec/fixtures/output.dat
106
+ - spec/fixtures/range_test.dat
106
107
  - spec/fixtures/timestamp.dat
107
108
  - spec/fixtures/timestamp_9.3.dat
108
109
  - spec/fixtures/timestamp_big.dat
@@ -135,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
136
  version: '0'
136
137
  requirements: []
137
138
  rubyforge_project:
138
- rubygems_version: 2.4.5.1
139
+ rubygems_version: 2.6.13
139
140
  signing_key:
140
141
  specification_version: 4
141
142
  summary: Convenient methods to load data quickly into Postgres
@@ -168,6 +169,7 @@ test_files:
168
169
  - spec/fixtures/just_an_array2.dat
169
170
  - spec/fixtures/multiline_hstore.dat
170
171
  - spec/fixtures/output.dat
172
+ - spec/fixtures/range_test.dat
171
173
  - spec/fixtures/timestamp.dat
172
174
  - spec/fixtures/timestamp_9.3.dat
173
175
  - spec/fixtures/timestamp_big.dat