diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/postgresql')
6 files changed, 213 insertions, 104 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index b7d24f2bb3..20de8d1982 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -2,6 +2,13 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column module ArrayParser + + DOUBLE_QUOTE = '"' + BACKSLASH = "\\" + COMMA = ',' + BRACKET_OPEN = '{' + BRACKET_CLOSE = '}' + private # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -12,18 +19,18 @@ module ActiveRecord include PgArrayParser rescue LoadError def parse_pg_array(string) - parse_data(string, 0) + parse_data(string) end end - def parse_data(string, index) - local_index = index + def parse_data(string) + local_index = 0 array = [] while(local_index < string.length) case string[local_index] - when '{' + when BRACKET_OPEN local_index,array = parse_array_contents(array, string, local_index + 1) - when '}' + when BRACKET_CLOSE return array end local_index += 1 @@ -33,9 +40,9 @@ module ActiveRecord end def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false + is_escaping = false + is_quoted = false + was_quoted = false current_item = '' local_index = index @@ -47,29 +54,29 @@ module ActiveRecord else if is_quoted case token - when '"' + when DOUBLE_QUOTE is_quoted = false was_quoted = true - when "\\" + when BACKSLASH is_escaping = true else current_item << token end else case token - when "\\" + when BACKSLASH is_escaping = true - when ',' + when COMMA add_item_to_array(array, current_item, was_quoted) current_item = '' was_quoted = false - when '"' + when DOUBLE_QUOTE is_quoted = true - when '{' + when BRACKET_OPEN internal_items = [] local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) array.push(internal_items) - when '}' + when BRACKET_CLOSE add_item_to_array(array, current_item, was_quoted) return local_index,array else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 3d8f0b575c..ea44e818e5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -2,12 +2,23 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column module Cast + def point_to_string(point) + "(#{point[0]},#{point[1]})" + end + + def string_to_point(string) + if string[0] == '(' && string[-1] == ')' + string = string[1...-1] + end + string.split(',').map{ |v| Float(v) } + end + def string_to_time(string) return string unless String === string case string - when 'infinity'; 1.0 / 0.0 - when '-infinity'; -1.0 / 0.0 + when 'infinity'; Float::INFINITY + when '-infinity'; -Float::INFINITY when / BC$/ super("-" + string.sub(/ BC$/, "")) else @@ -15,6 +26,15 @@ module ActiveRecord end end + def string_to_bit(value) + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + end + def hstore_to_string(object) if Hash === object object.map { |k,v| @@ -30,8 +50,8 @@ module ActiveRecord nil elsif String === string Hash[string.scan(HstorePair).map { |k,v| - v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') [k,v] }] else @@ -40,7 +60,7 @@ module ActiveRecord end def json_to_string(object) - if Hash === object + if Hash === object || Array === object ActiveSupport::JSON.encode(object) else object @@ -80,7 +100,11 @@ module ActiveRecord if string.nil? nil elsif String === string - IPAddr.new(string) + begin + IPAddr.new(string) + rescue ArgumentError + nil + end else string end @@ -95,7 +119,7 @@ module ActiveRecord end def string_to_array(string, oid) - parse_pg_array(string).map{|val| oid.type_cast val} + parse_pg_array(string).map {|val| type_cast_array(oid, val)} end private @@ -126,6 +150,14 @@ module ActiveRecord "\"#{value.gsub(/"/,"\\\"")}\"" end end + + def type_cast_array(oid, value) + if ::Array === value + value.map {|item| type_cast_array(oid, item)} + else + oid.type_cast value + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 9b5170f657..fa173d13a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -134,34 +134,31 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - types = {} - result.fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } - end - - ret = ActiveRecord::Result.new(result.fields, result.values, types) - result.clear - return ret + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } end + + ret = ActiveRecord::Result.new(fields, result.values, types) + result.clear + return ret end def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + affected = result.cmd_tuples + result.clear + affected end alias :exec_update :exec_delete @@ -217,25 +214,6 @@ module ActiveRecord def rollback_db_transaction execute "ROLLBACK" end - - def outside_transaction? - message = "#outside_transaction? is deprecated. This method was only really used " \ - "internally, but you can use #transaction_open? instead." - ActiveSupport::Deprecation.warn message - @connection.transaction_status == PGconn::PQTRANS_IDLE - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 68f2f2ca7b..6c5792954f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -6,20 +6,27 @@ module ActiveRecord module OID class Type def type; end + end - def type_cast_for_write(value) + class Identity < Type + def type_cast(value) value end end - class Identity < Type + class Bit < Type def type_cast(value) - value + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_bit value + else + value + end end end class Bytea < Type def type_cast(value) + return if value.nil? PGconn.unescape_bytea value end end @@ -27,12 +34,17 @@ module ActiveRecord class Money < Type def type_cast(value) return if value.nil? + return value unless String === value # Because money output is formatted according to the locale, there are two # cases to consider (note the decimal separators): # (1) $12,345,678.12 # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + value.sub!(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, '') @@ -63,6 +75,16 @@ module ActiveRecord end end + class Point < Type + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_point value + else + value + end + end + end + class Array < Type attr_reader :subtype def initialize(subtype) @@ -203,11 +225,19 @@ module ActiveRecord end class Hstore < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class Cidr < Type @@ -219,11 +249,19 @@ module ActiveRecord end class Json < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_json value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class TypeMap @@ -312,14 +350,14 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'bit', 'text' - alias_type 'varbit', 'text' alias_type 'macaddr', 'text' alias_type 'uuid', 'text' register_type 'money', OID::Money.new register_type 'bytea', OID::Bytea.new register_type 'bool', OID::Boolean.new + register_type 'bit', OID::Bit.new + register_type 'varbit', OID::Bit.new register_type 'float4', OID::Float.new alias_type 'float8', 'float4' @@ -330,6 +368,7 @@ module ActiveRecord register_type 'time', OID::Time.new register_type 'path', OID::Identity.new + register_type 'point', OID::Point.new register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 47e2e3928f..e9daa5d7ff 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -18,27 +18,34 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column + sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) + case value when Range - if /range$/ =~ column.sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{column.sql_type}" + if /range$/ =~ sql_type + "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" else super end when Array - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + case sql_type + when 'point' then super(PostgreSQLColumn.point_to_string(value)) + when 'json' then super(PostgreSQLColumn.json_to_string(value)) else - super + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" + else + super + end end when Hash - case column.sql_type + case sql_type when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end when IPAddr - case column.sql_type + case sql_type when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) else super end @@ -51,11 +58,14 @@ module ActiveRecord super end when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" + if sql_type == 'money' || [:string, :text].include?(column.type) + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + else + super + end when String - case column.sql_type + case sql_type when 'bytea' then "'#{escape_bytea(value)}'" when 'xml' then "xml '#{quote_string(value)}'" when /^bit/ @@ -87,8 +97,13 @@ module ActiveRecord super(value, column) end when Array - return super(value, column) unless column.array - PostgreSQLColumn.array_to_string(value, column, self) + case column.sql_type + when 'point' then PostgreSQLColumn.point_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) + end when String return super(value, column) unless 'bytea' == column.sql_type { :value => value, :format => 1 } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 3bc61c5e0c..5dc70a5ad1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,6 +1,42 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + def visit_ColumnDefinition(o) + sql = super + if o.primary_key? && o.type == :uuid + sql << " PRIMARY KEY " + add_column_options!(sql, column_options(o)) + end + sql + end + + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' + end + + column = options.fetch(:column) { return super } + if column.type == :uuid && options[:default] =~ /\(\)/ + sql << " DEFAULT #{options[:default]}" + else + super + end + end + end + + def schema_creation + SchemaCreation.new self + end + module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -10,7 +46,7 @@ module ActiveRecord end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>, # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). # @@ -120,12 +156,15 @@ module ActiveRecord column_names = columns.values_at(*indkey).compact - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] + unless column_names.empty? + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) + end end.compact end @@ -282,6 +321,7 @@ module ActiveRecord result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE + WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1) @@ -293,7 +333,7 @@ module ActiveRecord JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' - AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval' + AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end @@ -337,18 +377,16 @@ module ActiveRecord # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) clear_cache! - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - - execute add_column_sql + super end # Changes the column of a table. def change_column(table_name, column_name, type, options = {}) clear_cache! quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) + sql_type << "[]" if options[:array] + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -375,6 +413,11 @@ module ActiveRecord rename_column_indexes(table_name, column_name, new_column_name) end + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" + end + def remove_index!(table_name, index_name) #:nodoc: execute "DROP INDEX #{quote_table_name(index_name)}" end @@ -425,22 +468,17 @@ module ActiveRecord end end - # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", ["posts.created_at desc"]) - # # => "DISTINCT posts.id, posts.created_at AS alias_0" - def distinct(columns, orders) #:nodoc: - order_columns = orders.map{ |s| + def columns_for_distinct(columns, orders) #:nodoc: + order_columns = orders.reject(&:blank?).map{ |s| # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super].concat(order_columns).join(', ') + [super, *order_columns].join(', ') end end end |