pg_conn 0.19.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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