foobara-postgresql-crud-driver 0.0.3 → 0.0.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/src/postgresql_crud_driver.rb +148 -74
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9da1f2e3a8d5170f4badb5d1ec0eeb8b6f443da56ce17b4b97911767620ae099
|
|
4
|
+
data.tar.gz: b12f27eca628e62f38c7f8d2a1af415d1ab3c72006a18d17f983b4fbb03eea91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7d0d2d19bc3cd5d3c1625b7b4958b0f160f9c733bdac7adc3f39ec09010ddad3f7fe8ce02aa9a9d4c9c6a7bc8216a731c13d299209947cf7ebb8a327a35fdb6
|
|
7
|
+
data.tar.gz: 8f4c6fa9014c60caef166ecdff36a22e7d87543276d36604da42ec8f7e95ff421547256e1d4e58b02f7ac789c5e5070b3f5fe0b3ec500406a1780fec49426fc7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [0.0.5] - 2025-11-10
|
|
2
|
+
|
|
3
|
+
- Improve error messages for foobara type/pg column incompatibilities
|
|
4
|
+
- Validate strings used as integers represent valid integers
|
|
5
|
+
|
|
6
|
+
## [0.0.4] - 2025-11-09
|
|
7
|
+
|
|
8
|
+
- Support a few more column types
|
|
9
|
+
- Make sure an entity -> entity association is cast properly
|
|
10
|
+
|
|
1
11
|
## [0.0.3] - 2025-10-21
|
|
2
12
|
|
|
3
13
|
- Pass through options to EntityAttributesCrudDriver instead of disallowing them
|
|
@@ -4,6 +4,18 @@ module Foobara
|
|
|
4
4
|
class NoSuchColumnOrTableError < StandardError; end
|
|
5
5
|
|
|
6
6
|
class UnsupportedPgColumnTypeError < StandardError
|
|
7
|
+
attr_accessor :pg_type, :attribute_name, :entity_class
|
|
8
|
+
|
|
9
|
+
def initialize(pg_type, entity_class)
|
|
10
|
+
# :nocov:
|
|
11
|
+
super("Unsupported column type #{pg_type} on #{entity_class.entity_name}")
|
|
12
|
+
# :nocov:
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class UnsupportedPgColumnTypeForAttributeError < StandardError
|
|
17
|
+
attr_accessor :pg_type, :attribute_name, :entity_class
|
|
18
|
+
|
|
7
19
|
def initialize(pg_type, attribute_name, entity_class)
|
|
8
20
|
# :nocov:
|
|
9
21
|
super("Unsupported column type #{pg_type} for attribute #{attribute_name} on #{entity_class.entity_name}")
|
|
@@ -11,12 +23,29 @@ module Foobara
|
|
|
11
23
|
end
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
class ColumnTypeMismatchError < StandardError
|
|
27
|
+
attr_accessor :pg_type, :foobara_type, :entity_class, :attribute_name
|
|
28
|
+
|
|
29
|
+
def initialize(pg_type:, foobara_type:, entity_class:, attribute_name: nil)
|
|
30
|
+
# TODO: figure out a way to test this code path
|
|
31
|
+
# :nocov:
|
|
32
|
+
self.pg_type = pg_type
|
|
33
|
+
self.foobara_type = foobara_type
|
|
34
|
+
self.entity_class = entity_class
|
|
35
|
+
self.attribute_name = attribute_name
|
|
36
|
+
|
|
37
|
+
super("Column type mismatch between foobara #{foobara_type.type_symbol} " \
|
|
38
|
+
"and postgres #{pg_type} for #{entity_class.entity_name}.#{attribute_name}")
|
|
39
|
+
# :nocov:
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
14
43
|
class << self
|
|
15
44
|
def get_transaction_number
|
|
16
45
|
@get_transaction_number ||= 0
|
|
17
46
|
@get_transaction_number += 1
|
|
18
47
|
if @get_transaction_number > 65_535
|
|
19
|
-
# TODO: test this
|
|
48
|
+
# TODO: test this code path somehow
|
|
20
49
|
# :nocov:
|
|
21
50
|
@get_transaction_number = 1
|
|
22
51
|
# :nocov:
|
|
@@ -225,6 +254,7 @@ module Foobara
|
|
|
225
254
|
SQL
|
|
226
255
|
|
|
227
256
|
raw_connection.exec(sql)
|
|
257
|
+
|
|
228
258
|
find(record_id)
|
|
229
259
|
end
|
|
230
260
|
|
|
@@ -267,9 +297,11 @@ module Foobara
|
|
|
267
297
|
info = column_info[attribute_name.to_s]
|
|
268
298
|
|
|
269
299
|
value = case info[:type]
|
|
270
|
-
when "integer", "
|
|
300
|
+
when "integer", "bigint",
|
|
301
|
+
"text", "character varying",
|
|
302
|
+
"timestamp without time zone"
|
|
271
303
|
value
|
|
272
|
-
when "jsonb"
|
|
304
|
+
when "jsonb", "json"
|
|
273
305
|
if value.nil?
|
|
274
306
|
unless info[:is_nullable]
|
|
275
307
|
# :nocov:
|
|
@@ -306,79 +338,20 @@ module Foobara
|
|
|
306
338
|
# :nocov:
|
|
307
339
|
end
|
|
308
340
|
|
|
309
|
-
pg_type = info[:type]
|
|
310
341
|
foobara_type = entity_class.model_type.element_types.element_types[attribute_name]
|
|
311
342
|
|
|
312
|
-
value =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
else
|
|
325
|
-
# :nocov:
|
|
326
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
327
|
-
# :nocov:
|
|
328
|
-
end
|
|
329
|
-
elsif foobara_type.extends?(:string) || foobara_type.extends?(:symbol)
|
|
330
|
-
case pg_type
|
|
331
|
-
when "text"
|
|
332
|
-
"'#{PG::Connection.escape(value.to_s)}'"
|
|
333
|
-
else
|
|
334
|
-
# :nocov:
|
|
335
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
336
|
-
# :nocov:
|
|
337
|
-
end
|
|
338
|
-
elsif foobara_type.extends?(:datetime)
|
|
339
|
-
case pg_type
|
|
340
|
-
when "timestamp without time zone"
|
|
341
|
-
"'#{PG::Connection.escape(value.inspect)}'"
|
|
342
|
-
else
|
|
343
|
-
# :nocov:
|
|
344
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
345
|
-
# :nocov:
|
|
346
|
-
end
|
|
347
|
-
elsif foobara_type.extends?(:model) || foobara_type.extends?(:attributes)
|
|
348
|
-
case pg_type
|
|
349
|
-
when "jsonb"
|
|
350
|
-
"'#{PG::Connection.escape(JSON.fast_generate(value))}'"
|
|
351
|
-
else
|
|
352
|
-
# :nocov:
|
|
353
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
354
|
-
# :nocov:
|
|
355
|
-
end
|
|
356
|
-
elsif foobara_type.extends?(:array)
|
|
357
|
-
element_type = foobara_type.element_type
|
|
358
|
-
|
|
359
|
-
if element_type.extends?(:detached_entity)
|
|
360
|
-
case pg_type
|
|
361
|
-
when "ARRAY"
|
|
362
|
-
elements_type = ARRAY_ELEMENT_ENCODERS[info[:element_type]]
|
|
363
|
-
array_string = PG::TextEncoder::Array.new(elements_type:).encode(value)
|
|
364
|
-
escaped = PG::Connection.escape(array_string)
|
|
365
|
-
|
|
366
|
-
"'#{escaped}'"
|
|
367
|
-
else
|
|
368
|
-
# :nocov:
|
|
369
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
370
|
-
# :nocov:
|
|
371
|
-
end
|
|
372
|
-
else
|
|
373
|
-
# :nocov:
|
|
374
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
375
|
-
# :nocov:
|
|
376
|
-
end
|
|
377
|
-
else
|
|
378
|
-
# :nocov:
|
|
379
|
-
raise UnsupportedPgColumnTypeError.new(pg_type, attribute_name, entity_class)
|
|
380
|
-
# :nocov:
|
|
381
|
-
end
|
|
343
|
+
value = begin
|
|
344
|
+
check_type_compatibility!(foobara_type, info, attribute_name:)
|
|
345
|
+
pg_cast_value(value, foobara_type, info)
|
|
346
|
+
rescue UnsupportedPgColumnTypeError => e
|
|
347
|
+
# :nocov:
|
|
348
|
+
raise UnsupportedPgColumnTypeForAttributeError.new(
|
|
349
|
+
e.pg_type,
|
|
350
|
+
attribute_name,
|
|
351
|
+
entity_class
|
|
352
|
+
)
|
|
353
|
+
# :nocov:
|
|
354
|
+
end
|
|
382
355
|
|
|
383
356
|
[PostgresqlCrudDriver.escape_identifier(attribute_name), value]
|
|
384
357
|
end
|
|
@@ -406,6 +379,107 @@ module Foobara
|
|
|
406
379
|
]
|
|
407
380
|
end
|
|
408
381
|
end
|
|
382
|
+
|
|
383
|
+
def pg_cast_value(value, foobara_type, pg_info)
|
|
384
|
+
pg_type = pg_info[:type]
|
|
385
|
+
|
|
386
|
+
if value.nil?
|
|
387
|
+
if pg_info[:is_nullable]
|
|
388
|
+
"NULL"
|
|
389
|
+
else
|
|
390
|
+
# :nocov:
|
|
391
|
+
raise "Unexpected nil value for #{attribute_name}"
|
|
392
|
+
# :nocov:
|
|
393
|
+
end
|
|
394
|
+
elsif foobara_type.extends?(:number)
|
|
395
|
+
validate_intable!(value)
|
|
396
|
+
value.to_i
|
|
397
|
+
elsif foobara_type.extends?(:string) || foobara_type.extends?(:symbol)
|
|
398
|
+
"'#{PG::Connection.escape(value.to_s)}'"
|
|
399
|
+
elsif foobara_type.extends?(:datetime)
|
|
400
|
+
"'#{PG::Connection.escape(value.inspect)}'"
|
|
401
|
+
elsif foobara_type.extends?(:detached_entity)
|
|
402
|
+
pg_cast_value(value, foobara_type.target_class.primary_key_type, pg_info)
|
|
403
|
+
elsif foobara_type.extends?(:model) || foobara_type.extends?(:attributes) ||
|
|
404
|
+
foobara_type == BuiltinTypes[:duck]
|
|
405
|
+
"'#{PG::Connection.escape(JSON.fast_generate(value))}'"
|
|
406
|
+
elsif foobara_type.extends?(:array)
|
|
407
|
+
element_type = foobara_type.element_type
|
|
408
|
+
|
|
409
|
+
if element_type.extends?(:detached_entity)
|
|
410
|
+
if element_type.target_class.primary_key_type.extends?(:integer)
|
|
411
|
+
value.each { validate_intable!(it) }
|
|
412
|
+
end
|
|
413
|
+
elements_type = ARRAY_ELEMENT_ENCODERS[pg_info[:element_type]]
|
|
414
|
+
array_string = PG::TextEncoder::Array.new(elements_type:).encode(value)
|
|
415
|
+
escaped = PG::Connection.escape(array_string)
|
|
416
|
+
|
|
417
|
+
"'#{escaped}'"
|
|
418
|
+
else
|
|
419
|
+
# :nocov:
|
|
420
|
+
raise UnsupportedPgColumnTypeError.new(pg_type, entity_class)
|
|
421
|
+
# :nocov:
|
|
422
|
+
end
|
|
423
|
+
else
|
|
424
|
+
# :nocov:
|
|
425
|
+
raise UnsupportedPgColumnTypeError.new(pg_type, entity_class)
|
|
426
|
+
# :nocov:
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def validate_intable!(value)
|
|
431
|
+
if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
432
|
+
unless value =~ /\A[+-]?\d+\z/
|
|
433
|
+
# :nocov:
|
|
434
|
+
raise "Expected something that could be cast to an integer but got #{value}"
|
|
435
|
+
# :nocov:
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def check_type_compatibility!(foobara_type, pg_info, pg_type: pg_info[:type], attribute_name: nil)
|
|
441
|
+
if foobara_type.extends?(:integer)
|
|
442
|
+
unless pg_type == "integer" || pg_type == "bigint" || pg_type == "_int4"
|
|
443
|
+
# :nocov:
|
|
444
|
+
raise ColumnTypeMismatchError.new(pg_type:, foobara_type:, attribute_name:, entity_class:)
|
|
445
|
+
# :nocov:
|
|
446
|
+
end
|
|
447
|
+
elsif foobara_type.extends?(:string) || foobara_type.extends?(:symbol)
|
|
448
|
+
unless pg_type == "text" || pg_type == "character varying"
|
|
449
|
+
# :nocov:
|
|
450
|
+
raise ColumnTypeMismatchError.new(pg_type:, foobara_type:, attribute_name:, entity_class:)
|
|
451
|
+
# :nocov:
|
|
452
|
+
end
|
|
453
|
+
elsif foobara_type.extends?(:datetime)
|
|
454
|
+
unless pg_type == "timestamp without time zone"
|
|
455
|
+
# :nocov:
|
|
456
|
+
raise ColumnTypeMismatchError.new(pg_type:, foobara_type:, attribute_name:, entity_class:)
|
|
457
|
+
# :nocov:
|
|
458
|
+
end
|
|
459
|
+
elsif foobara_type.extends?(:detached_entity)
|
|
460
|
+
check_type_compatibility!(foobara_type.target_class.primary_key_type, pg_info, pg_type:, attribute_name:)
|
|
461
|
+
elsif foobara_type.extends?(:model) || foobara_type.extends?(:attributes) ||
|
|
462
|
+
foobara_type == BuiltinTypes[:duck]
|
|
463
|
+
unless pg_type == "jsonb" || pg_type == "json"
|
|
464
|
+
# :nocov:
|
|
465
|
+
raise ColumnTypeMismatchError.new(pg_type:, foobara_type:, attribute_name:, entity_class:)
|
|
466
|
+
# :nocov:
|
|
467
|
+
end
|
|
468
|
+
elsif foobara_type.extends?(:array)
|
|
469
|
+
if pg_type == "ARRAY"
|
|
470
|
+
element_type = foobara_type.element_type
|
|
471
|
+
check_type_compatibility!(element_type, pg_info, pg_type: pg_info[:element_type], attribute_name:)
|
|
472
|
+
# :nocov:
|
|
473
|
+
elsif pg_type != json && pg_type != jsonb
|
|
474
|
+
raise ColumnTypeMismatchError.new(pg_type:, foobara_type:, attribute_name:, entity_class:)
|
|
475
|
+
# :nocov:
|
|
476
|
+
end
|
|
477
|
+
else
|
|
478
|
+
# :nocov:
|
|
479
|
+
raise UnsupportedPgColumnTypeError.new(pg_type, entity_class)
|
|
480
|
+
# :nocov:
|
|
481
|
+
end
|
|
482
|
+
end
|
|
409
483
|
end
|
|
410
484
|
end
|
|
411
485
|
end
|