pg_conn 0.19.0 → 0.21.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/TODO +1 -0
  3. data/lib/pg_conn/version.rb +1 -1
  4. data/lib/pg_conn.rb +70 -64
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91bfceabfdba86eba789108cea2718e382fe6849b580ef3be3a428a8f08ab45e
4
- data.tar.gz: 0014ae1ccc9c6cf95f63fce78d5a094b7068e711ed18d0edcdaafe35d47ef04b
3
+ metadata.gz: 6f293ad25581975cbd57dc5973d072043d6b5cf13093a21bd354720b39c91c12
4
+ data.tar.gz: 1ad472d136a9b626717f90058cb220eaaf4c87bced2f8802c8cab68b69bf187a
5
5
  SHA512:
6
- metadata.gz: 6302b2051fd7a152b026ed7a412ad9025ae7fbdb6f260173c5731c123f367c7535a9522e2654ccc09950d14c9e3009c1daf6d52fa7fc3114c3b9f196e6db0570
7
- data.tar.gz: c793534b923e08cc22121dc824c858003cb495e2453cb77cd7007672e242a62ff8819ab966f2f9d70a552f3a080da2627c1ec1211990eefcbe87bdcc1a5c06e8
6
+ metadata.gz: 7720aef7f128e0b8e3c47fdd2acb752b1d44af9625653eae0552a2ac619e3c29ae2cad25789f0f0806fe4576104a64210cf5a5d7e7c7fa66817b8538a556bd35
7
+ data.tar.gz: 61425eaeabb6b6e0872f6ed49064f8650361ecd8be684b7db2322a6a346265e253489e49be37688c9ef74171733d2c8f19a12d38105a188ba263cf5ea48c9593
data/TODO CHANGED
@@ -1,4 +1,5 @@
1
1
  TODO
2
+ o "use 'drop ... cascade' everywhere
2
3
  o db.context(schema: app_portal, transaction: true) { ... }
3
4
  db.context(set: app_portal, transaction: true) { ... }
4
5
  db.context(add: app_portal, transaction: true) { ... }
@@ -1,3 +1,3 @@
1
1
  module PgConn
2
- VERSION = "0.19.0"
2
+ VERSION = "0.21.0"
3
3
  end
data/lib/pg_conn.rb CHANGED
@@ -64,9 +64,13 @@ module PgConn
64
64
  attr_reader :session
65
65
 
66
66
  # The transaction timestamp of the most recent SQL statement executed by
67
- # #exec or #transaction block
67
+ # #exec or #transaction block. The timestamp is without time zone
68
68
  attr_reader :timestamp
69
69
 
70
+ # The transaction timestamp of the most recent SQL statement executed by
71
+ # #exec or #transaction block. The timestamp includes the current time zone
72
+ attr_reader :timestamptz
73
+
70
74
  # PG::Error object of the first failed statement in the transaction;
71
75
  # otherwise nil. It is cleared at the beginning of a transaction so be sure
72
76
  # to save it before you run any cleanup code that may initiate new
@@ -139,6 +143,7 @@ module PgConn
139
143
  if args.last.is_a?(Hash)
140
144
  @field_name_class = args.last.delete(:field_name_class) || Symbol
141
145
  @timestamp = args.last.delete(:timestamp)
146
+ @timestamptz = args.last.delete(:timestamptz)
142
147
  args.pop if args.last.empty?
143
148
  else
144
149
  @field_name_class = Symbol
@@ -185,7 +190,7 @@ module PgConn
185
190
  @pg_connection.set_notice_processor { |message| ; } # Intentionally a nop
186
191
 
187
192
  # Auto-convert to ruby types
188
- type_map = PG::BasicTypeMapForResults.new(@pg_connection)
193
+ type_map = PG::BasicTypeMapForResults.new(@pg_connection)
189
194
 
190
195
  # Use String as default type. Kills 'Warning: no type cast defined for
191
196
  # type "uuid" with oid 2950..' warnings
@@ -193,7 +198,7 @@ module PgConn
193
198
 
194
199
  # Timestamp decoder
195
200
  type_map.add_coder PG::TextDecoder::Timestamp.new( # Timestamp without time zone
196
- oid: 1114,
201
+ oid: 1114,
197
202
  flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC)
198
203
 
199
204
  # Decode anonymous records but note that this is only useful to convert the
@@ -212,13 +217,12 @@ module PgConn
212
217
  @role = RoleMethods.new(self)
213
218
  @rdbms = RdbmsMethods.new(self)
214
219
  @session = SessionMethods.new(self)
215
- @timestamp = nil
216
220
  @savepoints = nil # Stack of savepoint names. Nil if no transaction in progress
217
221
  end
218
222
 
219
223
  # Close the database connection. TODO: Rename 'close'
220
224
  def terminate()
221
- @pg_connection.close if @pg_connection && !@pg_connection.finished?
225
+ @pg_connection.close if @pg_connection && !@pg_connection.finished?
222
226
  end
223
227
 
224
228
  def self.new(*args, **opts, &block)
@@ -238,7 +242,7 @@ module PgConn
238
242
  # Quote value as an identifier. Value should be a non-nil string or a symbol
239
243
  def quote_identifier(s)
240
244
  s = s.to_s if s.is_a?(Symbol)
241
- String@pg_connection.escape_identifier(s)
245
+ @pg_connection.escape_identifier(s)
242
246
  end
243
247
 
244
248
  # Quote the value as a string. Emit 'null' if value is nil
@@ -247,7 +251,11 @@ module PgConn
247
251
  # before quoting. This works by default for the regular types Integer,
248
252
  # true/false, Time/Date/DateTime, and arrays. Other types may require
249
253
  # special handling
250
- def quote_value(value)
254
+ #
255
+ # Note that a tuple value (an array) must be quoted using #quote_tuple
256
+ # because #quote_value would quote the tuple as an array instead of a list
257
+ # of values
258
+ def quote_value(value)
251
259
  case value
252
260
  when String; @pg_connection.escape_literal(value)
253
261
  when Integer, Float; value.to_s
@@ -255,28 +263,23 @@ module PgConn
255
263
  when nil; 'null'
256
264
  when Date, DateTime; "'#{value}'"
257
265
  when Time; "'#{value.strftime("%FT%T%:z")}'"
258
- when Array; "array[#{value.map { |elem| quote_literal(elem) }.join(', ')}]"
266
+ when Array; "array[#{value.map { |elem| quote_value(elem) }.join(', ')}]"
259
267
  else
260
268
  @pg_connection.escape_literal(value.to_s)
261
269
  end
262
270
  end
263
271
 
272
+ # Quote an array of values as a tuple. Just an alias for #quote_values
273
+ def quote_tuple(tuple) = quote_values(tuple)
264
274
 
265
- # Quote array as a comma-separated sequence of identifiers
266
- def quote_identifier_seq(identifiers) = identifiers.map { quote_identifier(_1) }.join(', ')
267
-
268
- # Quote array as a parenthesis-enclosed sequence of identifiers
269
- def quote_identifier_list(identifiers) = "(#{quote_identifier_seq(identifiers)})"
270
-
271
- # Quote array as a comma-separated sequence of values
272
- def quote_value_seq(values) = values.map { quote_literal(_1) }.join(', ')
275
+ # Quote identifiers and concatenate them using ',' as separator
276
+ def quote_identifiers(idents) = idents.map { |ident| quote_identifier(ident) }.join(", ")
273
277
 
274
- # Quote array as a parenthesis-enclosed list of value
275
- def quote_value_list(values) = "(#{quote_value_seq(values)})"
278
+ # Quote values and concatenate them using ',' as separator
279
+ def quote_values(values) = values.map { |value| quote_value(value) }.join(", ")
276
280
 
277
- # Old aliases. TODO Remove
278
- def quote_literal(value) = quote_value(value)
279
- def quote_literal_list(values) = quote_value_list(values)
281
+ # Quote an array of tuples
282
+ def quote_tuples(tuples) = tuples.map { |tuple| "(#{quote_values(tuple)})" }.join(", ")
280
283
 
281
284
  # :call-seq:
282
285
  # exist?(query)
@@ -287,7 +290,7 @@ module PgConn
287
290
  # check if the query returns one or more records
288
291
  def exist?(*args)
289
292
  arg1, arg2 = *args
290
- query =
293
+ query =
291
294
  case arg2
292
295
  when Integer; "select from #{arg1} where id = #{arg2}"
293
296
  when String; "select from #{arg1} where #{arg2}"
@@ -301,7 +304,7 @@ module PgConn
301
304
  # count(table, where_clause = nil)
302
305
  #
303
306
  # Return true if the table or the result of the query is empty
304
- def empty?(arg, where_clause = nil)
307
+ def empty?(arg, where_clause = nil)
305
308
  if arg =~ /\s/
306
309
  value "select count(*) from (#{arg} limit 1) as inner_query"
307
310
  elsif where_clause
@@ -324,6 +327,12 @@ module PgConn
324
327
  end
325
328
  end
326
329
 
330
+ # TODO
331
+ # Query variants
332
+ # (sql) - simple SQL statement
333
+ # (schema = nil, table, id-or-where-clause = nil, field-or-fields)
334
+ #
335
+
327
336
  # Return a single value. It is an error if the query doesn't return a
328
337
  # single record with a single column. If :transaction is true, the query
329
338
  # will be executed in a transaction and also be committed if :commit is
@@ -551,23 +560,12 @@ module PgConn
551
560
  # columns (like #tuples)
552
561
  #
553
562
  def call(name, *args, proc: false) # :proc may interfere with hashes
554
- args_sql = args.map { |arg| # TODO: Use pg's encoder
555
- case arg
556
- when NilClass; "null"
557
- when String; "'#{arg}'"
558
- when Integer; arg
559
- when TrueClass, FalseClass; arg
560
- when Array; "Array['#{arg.join("', '")}']" # Quick and dirty # FIXME
561
- when Hash; raise NotImplementedError
562
- else
563
- raise ArgumentError, "Unrecognized value: #{arg.inspect}"
564
- end
565
- }.join(", ")
563
+ args_seq = quote_values(args)
566
564
  if proc
567
- pg_exec "call #{name}(#{args_sql})"
565
+ pg_exec "call #{name}(#{args_seq})"
568
566
  return nil
569
567
  else
570
- r = pg_exec "select * from #{name}(#{args_sql})"
568
+ r = pg_exec "select * from #{name}(#{args_seq})"
571
569
  if r.ntuples == 0
572
570
  raise Error, "No records returned"
573
571
  elsif r.ntuples == 1
@@ -611,15 +609,14 @@ module PgConn
611
609
  table = schema ? "#{schema}.#{table}" : table
612
610
 
613
611
  # Find method and normalize data
614
- if data.is_a?(Array)
612
+ if data.is_a?(Array) # Array of tuples
613
+ method = :values
615
614
  if data.empty?
616
615
  return []
617
- elsif data.first.is_a?(Array)
618
- method = :values
616
+ elsif data.first.is_a?(Array) # Tuple (array) element. Requires the 'fields' argument
619
617
  fields or raise ArgumentError
620
618
  tuples = data
621
- elsif data.first.is_a?(Hash)
622
- method = :values
619
+ elsif data.first.is_a?(Hash) # Hash element
623
620
  fields ||= data.first.keys
624
621
  tuples = data.map { |record| fields.map { |field| record[field] } }
625
622
  else
@@ -633,21 +630,23 @@ module PgConn
633
630
  raise ArgumentError
634
631
  end
635
632
 
636
- # Build and execute SQL statement
637
- columns = quote_identifier_list(fields)
638
- values = tuples.map { |tuple| quote_literal_list(tuple) }.join(', ')
639
- self.send method, %(insert into #{table} #{columns} values #{values} returning id)
633
+ # Execute SQL statement using either :value or :values depending on data arity
634
+ self.send method, %(
635
+ insert into #{table} (#{quote_identifiers(fields)})
636
+ values #{quote_tuples(tuples)}
637
+ returning id
638
+ )
640
639
  end
641
640
 
642
641
  # Update record(s)
643
642
  def update(schema = nil, table, expr, hash)
644
643
  table = [schema, table].compact.join(".")
645
- assignments = hash.map { |k,v| "#{k} = #{quote_literal(v)}" }.join(", ")
646
- constraint =
644
+ assignments = hash.map { |k,v| "#{k} = #{quote_value(v)}" }.join(", ")
645
+ constraint =
647
646
  case expr
648
647
  when String; expr
649
- when Integer; "id = #{quote_literal(expr)}"
650
- when Array; "id in #{quote_literal_list(expr)}"
648
+ when Integer; "id = #{quote_value(expr)}"
649
+ when Array; "id in (#{quote_values(expr)})"
651
650
  else
652
651
  raise ArgumentError
653
652
  end
@@ -657,11 +656,11 @@ module PgConn
657
656
  # Delete record(s)
658
657
  def delete(schema = nil, table, expr)
659
658
  table = [schema, table].compact.join(".")
660
- constraint =
659
+ constraint =
661
660
  case expr
662
661
  when String; expr
663
- when Integer; "id = #{quote_literal(expr)}"
664
- when Array; "id in #{quote_literal_list(expr)}"
662
+ when Integer; "id = #{quote_value(expr)}"
663
+ when Array; "id in (#{quote_values(expr)})"
665
664
  else
666
665
  raise ArgumentError
667
666
  end
@@ -676,7 +675,7 @@ module PgConn
676
675
  # is not.
677
676
  #
678
677
  # #exec pass Postgres exceptions to the caller unless :fail is false in which case
679
- # it returns nil.
678
+ # it returns nil.
680
679
  #
681
680
  # Note that postgres crashes the whole transaction stack if any error is
682
681
  # met so if you're inside a transaction, the transaction will be in an
@@ -692,7 +691,7 @@ module PgConn
692
691
  # There is not a corresponding #execute? method because any failure rolls
693
692
  # back the whole transaction stack. TODO: Check which exceptions that
694
693
  # should be captured
695
- def exec?(sql, commit: true, silent: true)
694
+ def exec?(sql, commit: true, silent: true)
696
695
  begin
697
696
  exec(sql, commit: commit, fail: true, silent: silent)
698
697
  rescue PG::Error
@@ -727,7 +726,7 @@ module PgConn
727
726
  # back to the original user
728
727
  #
729
728
  # FIXME: The out-commented transaction block makes postspec fail for some reason
730
- # TODO: Rename 'sudo' because it acts just like it.
729
+ # TODO: Rename 'sudo' because it acts just like it.
731
730
  def su(username, &block)
732
731
  raise Error, "Missing block in call to PgConn::Connection#su" if !block_given?
733
732
  realuser = self.value "select current_user"
@@ -776,7 +775,14 @@ module PgConn
776
775
  @savepoints = []
777
776
  pg_exec("begin")
778
777
  @error = @err = nil
779
- @timestamp = pg_exec("select current_timestamp").values[0][0] if @pg_connection
778
+ # FIXME This special-cases the situation where commands are logged to a
779
+ # file instead of being executed. Maybe remove logging (or execute always
780
+ # and log as a side-effect)
781
+ if @pg_connection
782
+ @timestamp, @timestamptz = @pg_connection.exec(
783
+ 'select current_timestamp, current_timestamp::timestamp without time zone'
784
+ ).tuple_values(0)
785
+ end
780
786
  end
781
787
  end
782
788
 
@@ -913,7 +919,7 @@ module PgConn
913
919
  @error = ex
914
920
  @err = nil
915
921
  end
916
- if !silent # FIXME Why do we handle this?
922
+ if !silent # FIXME Why do we handle this?
917
923
  $stderr.puts arg
918
924
  $stderr.puts
919
925
  $stderr.puts ex.message
@@ -933,20 +939,20 @@ module PgConn
933
939
  end
934
940
  end
935
941
 
936
- def check_1c(r)
942
+ def check_1c(r)
937
943
  case r.nfields
938
944
  when 0; raise Error, "No columns returned"
939
- when 1;
945
+ when 1;
940
946
  else
941
- raise Error, "More than one column returned"
947
+ raise Error, "More than one column returned"
942
948
  end
943
949
  end
944
950
 
945
- def check_1r(r)
951
+ def check_1r(r)
946
952
  if r.ntuples == 0
947
953
  raise Error, "No records returned"
948
- elsif r.ntuples > 1
949
- raise Error, "More than one record returned"
954
+ elsif r.ntuples > 1
955
+ raise Error, "More than one record returned"
950
956
  end
951
957
  end
952
958
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_conn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-11 00:00:00.000000000 Z
11
+ date: 2024-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg