lib/pg_conn.rb in pg_conn-0.20.0 vs lib/pg_conn.rb in pg_conn-0.21.0

- old
+ new

@@ -65,11 +65,11 @@ # The transaction timestamp of the most recent SQL statement executed by # #exec or #transaction block. The timestamp is without time zone attr_reader :timestamp - # The transaction timestamp of the most recent SQL statement executed by + # The transaction timestamp of the most recent SQL statement executed by # #exec or #transaction block. The timestamp includes the current time zone attr_reader :timestamptz # PG::Error object of the first failed statement in the transaction; # otherwise nil. It is cleared at the beginning of a transaction so be sure @@ -188,19 +188,19 @@ if @pg_connection && !using_existing_connection # Set a dummy notice processor to avoid warnings on stderr @pg_connection.set_notice_processor { |message| ; } # Intentionally a nop # Auto-convert to ruby types - type_map = PG::BasicTypeMapForResults.new(@pg_connection) + type_map = PG::BasicTypeMapForResults.new(@pg_connection) # Use String as default type. Kills 'Warning: no type cast defined for # type "uuid" with oid 2950..' warnings type_map.default_type_map = PG::TypeMapAllStrings.new # Timestamp decoder type_map.add_coder PG::TextDecoder::Timestamp.new( # Timestamp without time zone - oid: 1114, + oid: 1114, flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC) # Decode anonymous records but note that this is only useful to convert the # outermost structure into an array, the elements are not decoded and are # returned as strings. It is best to avoid anonymous records if possible @@ -220,11 +220,11 @@ @savepoints = nil # Stack of savepoint names. Nil if no transaction in progress end # Close the database connection. TODO: Rename 'close' def terminate() - @pg_connection.close if @pg_connection && !@pg_connection.finished? + @pg_connection.close if @pg_connection && !@pg_connection.finished? end def self.new(*args, **opts, &block) if block_given? begin @@ -249,11 +249,15 @@ # # The value can be of any type but is converted to a string using #to_s # before quoting. This works by default for the regular types Integer, # true/false, Time/Date/DateTime, and arrays. Other types may require # special handling - def quote_value(value) + # + # Note that a tuple value (an array) must be quoted using #quote_tuple + # because #quote_value would quote the tuple as an array instead of a list + # of values + def quote_value(value) case value when String; @pg_connection.escape_literal(value) when Integer, Float; value.to_s when true, false; value.to_s when nil; 'null' @@ -263,48 +267,32 @@ else @pg_connection.escape_literal(value.to_s) end end - # Quote array as a comma-separated sequence of identifiers - def quote_identifier_seq(identifiers) = identifiers.map { quote_identifier(_1) }.join(', ') + # Quote an array of values as a tuple. Just an alias for #quote_values + def quote_tuple(tuple) = quote_values(tuple) - # Quote array as a parenthesis-enclosed sequence of identifiers. TODO: Rename quote_identifier_tuple - def quote_identifier_list(identifiers) = "(#{quote_identifier_seq(identifiers)})" + # Quote identifiers and concatenate them using ',' as separator + def quote_identifiers(idents) = idents.map { |ident| quote_identifier(ident) }.join(", ") - # Quote array as a bracket-enclosed sequence of identifiers -# def quote_identifier_array(identifiers) = "(#{quote_identifier_seq(identifiers)})" + # Quote values and concatenate them using ',' as separator + def quote_values(values) = values.map { |value| quote_value(value) }.join(", ") - # Quote array as a curly-bracket-enclosed sequence of identifiers -# def quote_identifier_hash(identifiers) = "(#{quote_identifier_seq(identifiers)})" + # Quote an array of tuples + def quote_tuples(tuples) = tuples.map { |tuple| "(#{quote_values(tuple)})" }.join(", ") - # Quote array as a comma-separated sequence of quoted values - def quote_value_seq(values) = values.map { quote_literal(_1) }.join(', ') - - # Quote array as a parenthesis-enclosed list of quoted values. TODO: Rename to quote_value_tuple - def quote_value_list(values) = "(#{quote_value_seq(values)})" - - # Quote array as a bracket-enclosed sequence of values -# def quote_identifier_array(values) = "(#{quote_identifier_seq(values)})" - - # Quote array as a curly-bracket-enclosed sequence of values -# def quote_identifier_hash(values) = "(#{quote_identifier_seq(values)})" - - # Old aliases. TODO Remove - def quote_literal(value) = quote_value(value) - def quote_literal_list(values) = quote_value_list(values) - # :call-seq: # exist?(query) # exist?(table, id) # eists?(table, where_clause) # # Return true iff the query returns exactly one record. Use '!empty?' to # check if the query returns one or more records def exist?(*args) arg1, arg2 = *args - query = + query = case arg2 when Integer; "select from #{arg1} where id = #{arg2}" when String; "select from #{arg1} where #{arg2}" when NilClass; arg1 end @@ -314,11 +302,11 @@ # :call-seq: # count(query) # count(table, where_clause = nil) # # Return true if the table or the result of the query is empty - def empty?(arg, where_clause = nil) + def empty?(arg, where_clause = nil) if arg =~ /\s/ value "select count(*) from (#{arg} limit 1) as inner_query" elsif where_clause value "select count(*) from (select 1 from #{arg} where #{where_clause} limit 1) as inner_query" else @@ -570,27 +558,16 @@ # a tuple if the record has multiple columns (like #tuple), and an array of # of tuples if the result contained more than one record with multiple # columns (like #tuples) # def call(name, *args, proc: false) # :proc may interfere with hashes - args_sql = args.map { |arg| # TODO: Use pg's encoder - case arg - when NilClass; "null" - when String; "'#{arg}'" - when Integer; arg - when TrueClass, FalseClass; arg - when Array; "Array['#{arg.join("', '")}']" # Quick and dirty # FIXME - when Hash; raise NotImplementedError - else - raise ArgumentError, "Unrecognized value: #{arg.inspect}" - end - }.join(", ") + args_seq = quote_values(args) if proc - pg_exec "call #{name}(#{args_sql})" + pg_exec "call #{name}(#{args_seq})" return nil else - r = pg_exec "select * from #{name}(#{args_sql})" + r = pg_exec "select * from #{name}(#{args_seq})" if r.ntuples == 0 raise Error, "No records returned" elsif r.ntuples == 1 if r.nfields == 1 r.values[0][0] @@ -630,19 +607,18 @@ # Normalize table table = schema ? "#{schema}.#{table}" : table # Find method and normalize data - if data.is_a?(Array) + if data.is_a?(Array) # Array of tuples + method = :values if data.empty? return [] - elsif data.first.is_a?(Array) - method = :values + elsif data.first.is_a?(Array) # Tuple (array) element. Requires the 'fields' argument fields or raise ArgumentError tuples = data - elsif data.first.is_a?(Hash) - method = :values + elsif data.first.is_a?(Hash) # Hash element fields ||= data.first.keys tuples = data.map { |record| fields.map { |field| record[field] } } else raise ArgumentError end @@ -652,39 +628,41 @@ tuples = [fields.map { |field| data[field] }] else raise ArgumentError end - # Build and execute SQL statement - columns = quote_identifier_list(fields) - values = tuples.map { |tuple| quote_literal_list(tuple) }.join(', ') - self.send method, %(insert into #{table} #{columns} values #{values} returning id) + # Execute SQL statement using either :value or :values depending on data arity + self.send method, %( + insert into #{table} (#{quote_identifiers(fields)}) + values #{quote_tuples(tuples)} + returning id + ) end # Update record(s) def update(schema = nil, table, expr, hash) table = [schema, table].compact.join(".") - assignments = hash.map { |k,v| "#{k} = #{quote_literal(v)}" }.join(", ") - constraint = + assignments = hash.map { |k,v| "#{k} = #{quote_value(v)}" }.join(", ") + constraint = case expr when String; expr - when Integer; "id = #{quote_literal(expr)}" - when Array; "id in #{quote_literal_list(expr)}" + when Integer; "id = #{quote_value(expr)}" + when Array; "id in (#{quote_values(expr)})" else raise ArgumentError end exec %(update #{table} set #{assignments} where #{constraint}) end # Delete record(s) def delete(schema = nil, table, expr) table = [schema, table].compact.join(".") - constraint = + constraint = case expr when String; expr - when Integer; "id = #{quote_literal(expr)}" - when Array; "id in #{quote_literal_list(expr)}" + when Integer; "id = #{quote_value(expr)}" + when Array; "id in (#{quote_values(expr)})" else raise ArgumentError end exec %(delete from #{table} where #{constraint}) end @@ -695,11 +673,11 @@ # arbitrarily nested array of commands. Note that you can't have commands # that span multiple lines. The empty array is a NOP but the empty string # is not. # # #exec pass Postgres exceptions to the caller unless :fail is false in which case - # it returns nil. + # it returns nil. # # Note that postgres crashes the whole transaction stack if any error is # met so if you're inside a transaction, the transaction will be in an # error state and if you're also using subtransactions the whole # transaction stack has collapsed @@ -711,11 +689,11 @@ # Like #exec but returns true/false depending on if the command succeeded. # There is not a corresponding #execute? method because any failure rolls # back the whole transaction stack. TODO: Check which exceptions that # should be captured - def exec?(sql, commit: true, silent: true) + def exec?(sql, commit: true, silent: true) begin exec(sql, commit: commit, fail: true, silent: silent) rescue PG::Error return false end @@ -746,11 +724,11 @@ # Switch user to the given user and execute the statement before swithcing # back to the original user # # FIXME: The out-commented transaction block makes postspec fail for some reason - # TODO: Rename 'sudo' because it acts just like it. + # TODO: Rename 'sudo' because it acts just like it. def su(username, &block) raise Error, "Missing block in call to PgConn::Connection#su" if !block_given? realuser = self.value "select current_user" result = nil # transaction(commit: false) { @@ -796,17 +774,17 @@ else @savepoints = [] pg_exec("begin") @error = @err = nil # FIXME This special-cases the situation where commands are logged to a - # file instead of being executed. Maybe remove logging (or execute always + # file instead of being executed. Maybe remove logging (or execute always # and log as a side-effect) if @pg_connection @timestamp, @timestamptz = @pg_connection.exec( 'select current_timestamp, current_timestamp::timestamp without time zone' ).tuple_values(0) - end + end end end def pop_transaction(commit: true, fail: true, exception: true) if transaction? @@ -939,11 +917,11 @@ rescue PG::Error => ex if @error.nil? @error = ex @err = nil end - if !silent # FIXME Why do we handle this? + if !silent # FIXME Why do we handle this? $stderr.puts arg $stderr.puts $stderr.puts ex.message $stderr.flush end @@ -959,23 +937,23 @@ end nil end end - def check_1c(r) + def check_1c(r) case r.nfields when 0; raise Error, "No columns returned" - when 1; + when 1; else - raise Error, "More than one column returned" + raise Error, "More than one column returned" end end - def check_1r(r) + def check_1r(r) if r.ntuples == 0 raise Error, "No records returned" - elsif r.ntuples > 1 - raise Error, "More than one record returned" + elsif r.ntuples > 1 + raise Error, "More than one record returned" end end end def self.sql_values(values) "'" + values.join("', '") + "'" end