aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/postgresql
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/postgresql')
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb148
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb89
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb65
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb181
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb170
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb66
33 files changed, 669 insertions, 455 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index f7bad20f00..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module Cast # :nodoc:
- def point_to_string(point) # :nodoc:
- "(#{point[0]},#{point[1]})"
- end
-
- def string_to_bit(value) # :nodoc:
- 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, array_member = false) # :nodoc:
- if Hash === object
- string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',')
- string = escape_hstore(string) if array_member
- string
- else
- object
- end
- end
-
- def string_to_hstore(string) # :nodoc:
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k, v|
- 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
- string
- end
- end
-
- def json_to_string(object) # :nodoc:
- if Hash === object || Array === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter) # :nodoc:
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
- def range_to_string(object) # :nodoc:
- from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
- to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
- "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
- end
-
- def string_to_json(string) # :nodoc:
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- end
- end
-
- def string_to_cidr(string) # :nodoc:
- if string.nil?
- nil
- elsif String === string
- begin
- IPAddr.new(string)
- rescue ArgumentError
- nil
- end
- else
- string
- end
- end
-
- def cidr_to_string(object) # :nodoc:
- if IPAddr === object
- "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
- else
- object
- end
- end
-
- def string_to_array(string, oid) # :nodoc:
- parse_pg_array(string).map {|val| type_cast_array(oid, val)}
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
-
- def quote_and_escape(value)
- case value
- when "NULL", Numeric
- value
- else
- value = value.gsub(/\\/, ARRAY_ESCAPE)
- value.gsub!(/"/,"\\\"")
- "\"#{value}\""
- 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
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 9a5e2d05ef..1458fbf496 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -1,5 +1,3 @@
-require 'active_record/connection_adapters/postgresql/cast'
-
module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
@@ -18,26 +16,8 @@ module ActiveRecord
@default_function = default_function
end
- # :stopdoc:
- class << self
- include PostgreSQL::Cast
-
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- require 'active_record/connection_adapters/postgresql/array_parser'
- include PostgreSQL::ArrayParser
- end
- end
- # :startdoc:
-
- def accessor
- cast_type.accessor
+ def serial?
+ default_function && default_function =~ /\Anextval\(.*\)\z/
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 89a7257d77..d09468329a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -156,10 +156,6 @@ module ActiveRecord
end
end
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new "$#{index + 1}"
- end
-
def exec_query(sql, name = 'SQL', binds = [])
execute_and_clear(sql, name, binds) do |result|
types = {}
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 2494e19f84..d28a2b4fa0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -2,6 +2,7 @@ require 'active_record/connection_adapters/postgresql/oid/infinity'
require 'active_record/connection_adapters/postgresql/oid/array'
require 'active_record/connection_adapters/postgresql/oid/bit'
+require 'active_record/connection_adapters/postgresql/oid/bit_varying'
require 'active_record/connection_adapters/postgresql/oid/bytea'
require 'active_record/connection_adapters/postgresql/oid/cidr'
require 'active_record/connection_adapters/postgresql/oid/date'
@@ -13,6 +14,7 @@ require 'active_record/connection_adapters/postgresql/oid/hstore'
require 'active_record/connection_adapters/postgresql/oid/inet'
require 'active_record/connection_adapters/postgresql/oid/integer'
require 'active_record/connection_adapters/postgresql/oid/json'
+require 'active_record/connection_adapters/postgresql/oid/jsonb'
require 'active_record/connection_adapters/postgresql/oid/money'
require 'active_record/connection_adapters/postgresql/oid/point'
require 'active_record/connection_adapters/postgresql/oid/range'
@@ -20,6 +22,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string'
require 'active_record/connection_adapters/postgresql/oid/time'
require 'active_record/connection_adapters/postgresql/oid/uuid'
require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index 0e9dcd8c0c..c203e6c604 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -2,21 +2,96 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Array < Type::Value
- attr_reader :subtype
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
delegate :type, to: :subtype
- def initialize(subtype)
+ def initialize(subtype, delimiter = ',')
@subtype = subtype
+ @delimiter = delimiter
end
- def type_cast(value)
- if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
else
- value
+ super
end
end
+
+ def type_cast_from_user(value)
+ if value.is_a?(::String)
+ value = parse_pg_array(value)
+ end
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
+ end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 9b2d887d07..1dbb40ca1d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -2,14 +2,49 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bit < Type::String
+ class Bit < Type::Value # :nodoc:
+ def type
+ :bit
+ end
+
def type_cast(value)
if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_bit value
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
else
value
end
end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
new file mode 100644
index 0000000000..4c21097d48
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class BitVarying < OID::Bit # :nodoc:
+ def type
+ :bit_varying
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 36c53d8732..6bd1b8ecae 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -2,9 +2,11 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bytea < Type::Binary
- def cast_value(value)
- PGconn.unescape_bytea value
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ return value.to_s if value.is_a?(Type::Binary::Data)
+ PGconn.unescape_bytea(super)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index 507c3a62b0..222f10fa8f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -2,13 +2,42 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Cidr < Type::Value
+ class Cidr < Type::Value # :nodoc:
def type
:cidr
end
+ def type_cast_for_schema(value)
+ subnet_mask = value.instance_variable_get(:@mask_addr)
+
+ # If the subnet mask is equal to /32, don't output it
+ if subnet_mask == (2**32 - 1)
+ "\"#{value}\""
+ else
+ "\"#{value}/#{subnet_mask.to_s(2).count('1')}\""
+ end
+ end
+
+ def type_cast_for_database(value)
+ if IPAddr === value
+ "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ value
+ end
+ end
+
def cast_value(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
+ if value.nil?
+ nil
+ elsif String === value
+ begin
+ IPAddr.new(value)
+ rescue ArgumentError
+ nil
+ end
+ else
+ value
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
index 3c30ad5fec..1d8d264530 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Date < Type::Date
+ class Date < Type::Date # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index 9ccbf71159..b9e7894e5c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class DateTime < Type::DateTime
+ class DateTime < Type::DateTime # :nodoc:
include Infinity
def cast_value(value)
@@ -11,7 +11,8 @@ module ActiveRecord
when 'infinity' then ::Float::INFINITY
when '-infinity' then -::Float::INFINITY
when / BC$/
- super("-" + value.sub(/ BC$/, ""))
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index ed4b8911d9..43d22c8daf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Decimal < Type::Decimal
+ class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index 5fed8b0f89..77d5038efd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Enum < Type::Value
+ class Enum < Type::Value # :nodoc:
def type
:enum
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
index 9753d71461..78ef94b912 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -2,12 +2,12 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Float < Type::Float
+ class Float < Type::Float # :nodoc:
include Infinity
- def type_cast(value)
+ def cast_value(value)
case value
- when nil then nil
+ when ::Float then value
when 'Infinity' then ::Float::INFINITY
when '-Infinity' then -::Float::INFINITY
when 'NaN' then ::Float::NAN
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index 98f369a7f8..be4525c94f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -2,22 +2,56 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Hstore < Type::Value
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
def type
:hstore
end
- def type_cast_for_write(value)
- ConnectionAdapters::PostgreSQLColumn.hstore_to_string value
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ 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
+ value
+ end
end
- def cast_value(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
+ def type_cast_for_database(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
index 7ed8f5f031..96486fa65b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Inet < Cidr
+ class Inet < Cidr # :nodoc:
def type
:inet
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
index d438ffa140..e47780399a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- module Infinity
+ module Infinity # :nodoc:
def infinity(options = {})
options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
index 388d3dd9ed..59abdc0009 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Integer < Type::Integer
+ class Integer < Type::Integer # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index 42bf5656f4..e12ddd9901 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -2,17 +2,27 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Json < Type::Value
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
def type
:json
end
- def type_cast_for_write(value)
- ConnectionAdapters::PostgreSQLColumn.json_to_string value
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
end
- def cast_value(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_json value
+ def type_cast_for_database(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
end
def accessor
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
new file mode 100644
index 0000000000..380c50fc14
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Jsonb < Json # :nodoc:
+ def type
+ :jsonb
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ # Postgres does not preserve insignificant whitespaces when
+ # roundtripping jsonb columns. This causes some false positives for
+ # the comparison here. Therefore, we need to parse and re-dump the
+ # raw value here to ensure the insignificant whitespaces are
+ # consistent with our encoder's output.
+ raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ super(raw_old_value, new_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 697dceb7c2..df890c2ed6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -2,11 +2,15 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Money < Type::Decimal
+ class Money < Type::Decimal # :nodoc:
include Infinity
class_attribute :precision
+ def type
+ :money
+ end
+
def scale
2
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index f9531ddee3..bac8b01d6b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -2,17 +2,40 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Point < Type::String
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :point
+ end
+
def type_cast(value)
- if ::String === value
+ case value
+ when ::String
if value[0] == '(' && value[-1] == ')'
value = value[1...-1]
end
- value.split(',').map{ |v| Float(v) }
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
else
value
end
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index c2262c1599..961e6224c4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -1,8 +1,10 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Range < Type::Value
+ class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
def initialize(subtype, type)
@@ -10,22 +12,8 @@ module ActiveRecord
@type = type
end
- def extract_bounds(value)
- from, to = value[1..-2].split(',')
- {
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
- exclude_start: (value[0] == '('),
- exclude_end: (value[-1] == ')')
- }
- end
-
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- end
-
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.type_cast(value)
+ def type_cast_for_schema(value)
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
end
def cast_value(value)
@@ -39,16 +27,51 @@ module ActiveRecord
if !infinity?(from) && extracted[:exclude_start]
if from.respond_to?(:succ)
from = from.succ
- ActiveSupport::Deprecation.warn <<-MESSAGE
-Excluding the beginning of a Range is only partialy supported through `#succ`.
-This is not reliable and will be removed in the future.
- MESSAGE
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Excluding the beginning of a Range is only partialy supported
+ through `#succ`. This is not reliable and will be removed in
+ the future.
+ MSG
else
raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
end
end
::Range.new(from, to, extracted[:exclude_end])
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
index 7b1ca16bc4..b2a42e9ebb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class SpecializedString < Type::String
+ class SpecializedString < Type::String # :nodoc:
attr_reader :type
def initialize(type)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
index ea1f599b0f..8f0246eddb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Time < Type::Time
+ class Time < Type::Time # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 28f7a4eafb..9b3de41fab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module OID # :nodoc:
# This class uses the data from PostgreSQL pg_type table to build
# the OID -> Type mapping.
- # - OID is and integer representing the type.
+ # - OID is an integer representing the type.
# - Type is an OID::Type object.
# This class has side effects on the +store+ passed during initialization.
class TypeMapInitializer # :nodoc:
@@ -39,14 +39,14 @@ module ActiveRecord
end
def register_array_type(row)
- if subtype = @store.lookup(row['typelem'].to_i)
- register row['oid'], OID::Array.new(subtype)
+ register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype|
+ OID::Array.new(subtype, row['typdelim'])
end
end
def register_range_type(row)
- if subtype = @store.lookup(row['rngsubtype'].to_i)
- register row['oid'], OID::Range.new(subtype, row['typname'].to_sym)
+ register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype|
+ OID::Range.new(subtype, row['typname'].to_sym)
end
end
@@ -64,9 +64,13 @@ module ActiveRecord
end
end
- def register(oid, oid_type)
- oid = assert_valid_registration(oid, oid_type)
- @store.register_type(oid, oid_type)
+ def register(oid, oid_type = nil, &block)
+ oid = assert_valid_registration(oid, oid_type || block)
+ if block_given?
+ @store.register_type(oid, &block)
+ else
+ @store.register_type(oid, oid_type)
+ end
end
def alias_type(oid, target)
@@ -74,6 +78,14 @@ module ActiveRecord
@store.alias_type(oid, target)
end
+ def register_with_subtype(oid, target_oid)
+ if @store.key?(target_oid)
+ register(oid) do |_, *args|
+ yield @store.lookup(target_oid, *args)
+ end
+ end
+ end
+
def assert_valid_registration(oid, oid_type)
raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
oid.to_i
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 0ed5491887..97b4fd3d08 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -2,13 +2,17 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Uuid < Type::Value
+ class Uuid < Type::Value # :nodoc:
+ ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x
+
+ alias_method :type_cast_for_database, :type_cast_from_database
+
def type
:uuid
end
def type_cast(value)
- value.presence
+ value.to_s[ACCEPTABLE_UUID, 0]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
index 2f7d1be197..de4187b028 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Vector < Type::Value
+ class Vector < Type::Value # :nodoc:
attr_reader :delim, :subtype
# +delim+ corresponds to the `typdelim` column in the pg_types
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..334af7c598
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,28 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index ad12298013..607848884b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -14,128 +14,6 @@ module ActiveRecord
@connection.unescape_bytea(value) if value
end
- # Quotes PostgreSQL-specific data types for SQL input.
- 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$/ =~ sql_type
- "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}"
- else
- super
- end
- when Array
- case sql_type
- when 'point' then super(PostgreSQLColumn.point_to_string(value))
- when 'json' then super(PostgreSQLColumn.json_to_string(value))
- else
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
- else
- super
- end
- end
- when Hash
- 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 sql_type
- when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
- else super
- end
- when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
- "'#{value.to_s}'"
- else
- super
- end
- when Numeric
- 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 sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
- else
- super
- end
- end
-
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when Range
- if /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
- else
- super(value, column)
- end
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- case column.sql_type
- when 'point' then PostgreSQLColumn.point_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else
- if column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- else
- super(value, column)
- end
- end
- when String
- if 'bytea' == column.sql_type
- # Return a bind param hash with format as binary.
- # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
- # for more information
- { value: value, format: 1 }
- else
- super(value, column)
- end
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- when IPAddr
- if %w(inet cidr).include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
- else
- super(value, column)
- end
- else
- super(value, column)
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -150,12 +28,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- if schema
- "#{quote_column_name(schema)}.#{quote_column_name(table)}"
- else
- quote_column_name(table)
- end
+ Utils.extract_schema_qualified_name(name.to_s).quoted
end
def quote_table_name_for_assignment(table, attr)
@@ -170,15 +43,12 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
- result = super
- if value.acts_like?(:time) && value.respond_to?(:usec)
- result = "#{result}.#{sprintf("%06d", value.usec)}"
- end
-
- if value.year < 0
- result = result.sub(/^-/, "") + " BC"
+ if value.year <= 0
+ bce_year = format("%04d", -value.year + 1)
+ super.sub(/^-?\d+/, bce_year) + " BC"
+ else
+ super
end
- result
end
# Does not quote function default values for UUID columns
@@ -189,6 +59,45 @@ module ActiveRecord
quote(value, column)
end
end
+
+ private
+
+ def _quote(value)
+ case value
+ when Type::Binary::Data
+ "'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ when Float
+ if value.infinite? || value.nan?
+ "'#{value}'"
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index bcfd605165..a9522e152f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -4,68 +4,88 @@ module ActiveRecord
module ColumnMethods
def xml(*args)
options = args.extract_options!
- column(args[0], 'xml', options)
+ column(args[0], :xml, options)
end
def tsvector(*args)
options = args.extract_options!
- column(args[0], 'tsvector', options)
+ column(args[0], :tsvector, options)
end
def int4range(name, options = {})
- column(name, 'int4range', options)
+ column(name, :int4range, options)
end
def int8range(name, options = {})
- column(name, 'int8range', options)
+ column(name, :int8range, options)
end
def tsrange(name, options = {})
- column(name, 'tsrange', options)
+ column(name, :tsrange, options)
end
def tstzrange(name, options = {})
- column(name, 'tstzrange', options)
+ column(name, :tstzrange, options)
end
def numrange(name, options = {})
- column(name, 'numrange', options)
+ column(name, :numrange, options)
end
def daterange(name, options = {})
- column(name, 'daterange', options)
+ column(name, :daterange, options)
end
def hstore(name, options = {})
- column(name, 'hstore', options)
+ column(name, :hstore, options)
end
def ltree(name, options = {})
- column(name, 'ltree', options)
+ column(name, :ltree, options)
end
def inet(name, options = {})
- column(name, 'inet', options)
+ column(name, :inet, options)
end
def cidr(name, options = {})
- column(name, 'cidr', options)
+ column(name, :cidr, options)
end
def macaddr(name, options = {})
- column(name, 'macaddr', options)
+ column(name, :macaddr, options)
end
def uuid(name, options = {})
- column(name, 'uuid', options)
+ column(name, :uuid, options)
end
def json(name, options = {})
- column(name, 'json', options)
+ column(name, :json, options)
+ end
+
+ def jsonb(name, options = {})
+ column(name, :jsonb, options)
end
def citext(name, options = {})
- column(name, 'citext', options)
+ column(name, :citext, options)
+ end
+
+ def point(name, options = {})
+ column(name, :point, options)
+ end
+
+ def bit(name, options)
+ column(name, :bit, options)
+ end
+
+ def bit_varying(name, options)
+ column(name, :bit_varying, options)
+ end
+
+ def money(name, options)
+ column(name, :money, options)
end
end
@@ -105,18 +125,14 @@ module ActiveRecord
# a record (as primary keys cannot be +nil+). This might be done via the
# +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
def primary_key(name, type = :primary_key, options = {})
- return super unless type == :uuid
- options[:default] = options.fetch(:default, 'uuid_generate_v4()')
- options[:primary_key] = true
- column name, type, options
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid
+ super
end
- def column(name, type = nil, options = {})
- super
- column = self[name]
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
column.array = options[:array]
-
- self
+ column
end
private
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 9e53d10bb4..a90adcf4aa 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -4,29 +4,30 @@ module ActiveRecord
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))
+ def column_options(o)
+ column_options = super
+ column_options[:array] = o.array
+ column_options
end
- def visit_ColumnDefinition(o)
- sql = super
- if o.primary_key? && o.type != :primary_key
- sql << " PRIMARY KEY "
- add_column_options!(sql, column_options(o))
+ def add_column_options!(sql, options)
+ if options[:array]
+ sql << '[]'
end
- sql
+ super
end
- def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
+ def quote_default_expression(value, column)
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ super
end
+ end
- column = options.fetch(:column) { return super }
- if column.type == :uuid && options[:default] =~ /\(\)/
- sql << " DEFAULT #{options[:default]}"
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
else
super
end
@@ -52,8 +53,8 @@ module ActiveRecord
def create_database(name, options = {})
options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
- option_string = options.sum do |key, value|
- case key
+ option_string = options.inject("") do |memo, (key, value)|
+ memo += case key
when :owner
" OWNER = \"#{value}\""
when :template
@@ -97,19 +98,23 @@ module ActiveRecord
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
+ name = Utils.extract_schema_qualified_name(name.to_s)
+ return false unless name.identifier
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
+ def drop_table(table_name, options = {})
+ execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
+ end
+
# Returns true if schema exists.
def schema_exists?(name)
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
@@ -179,12 +184,16 @@ module ActiveRecord
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
- default_value = extract_value_from_default(default)
+ default_value = extract_value_from_default(oid, default)
default_function = extract_default_function(default_value, default)
- PostgreSQLColumn.new(column_name, default_value, oid, type, notnull == 'f', default_function)
+ new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
end
end
+ def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ end
+
# Returns the current database name.
def current_database
query('select current_database()', 'SCHEMA')[0][0]
@@ -269,9 +278,9 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- result.split('.').last
+ Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
end
def serial_sequence(table, column)
@@ -281,6 +290,23 @@ module ActiveRecord
result.rows.first.first
end
+ # Sets the sequence of a table's primary key to the specified value.
+ def set_pk_sequence!(table, value) #:nodoc:
+ pk, sequence = pk_and_sequence_for(table)
+
+ if pk
+ if sequence
+ quoted_sequence = quote_table_name(sequence)
+
+ select_value <<-end_sql, 'SCHEMA'
+ SELECT setval('#{quoted_sequence}', #{value})
+ end_sql
+ else
+ @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ end
+ end
+ end
+
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
@@ -308,17 +334,19 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname, seq.relname
+ SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_constraint cons
+ pg_constraint cons,
+ pg_namespace nsp
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
+ AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
@@ -326,7 +354,7 @@ module ActiveRecord
if result.nil? or result.empty?
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname,
+ SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
@@ -338,33 +366,39 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
- [result.first, result.last]
+ pk = result.shift
+ if result.last
+ [pk, PostgreSQL::Name.new(*result)]
+ else
+ [pk, nil]
+ end
rescue
nil
end
# Returns just a table's primary key
def primary_key(table)
- row = exec_query(<<-end_sql, 'SCHEMA').rows.first
+ pks = exec_query(<<-end_sql, 'SCHEMA').rows
SELECT attr.attname
FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
WHERE cons.contype = 'p'
AND cons.conrelid = '#{quote_table_name(table)}'::regclass
end_sql
-
- row && row.first
+ return nil unless pks.count == 1
+ pks[0][0]
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
@@ -372,9 +406,12 @@ module ActiveRecord
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq == "#{table_name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
+ idx = "#{table_name}_pkey"
+ new_idx = "#{new_name}_pkey"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
end
rename_table_indexes(table_name, new_name)
@@ -393,7 +430,12 @@ module ActiveRecord
quoted_table_name = quote_table_name(table_name)
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}"
+ sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
+ sql << " USING #{options[:using]}" if options[:using]
+ if options[:cast_as]
+ sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})"
+ end
+ execute sql
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)
@@ -403,8 +445,16 @@ module ActiveRecord
def change_column_default(table_name, column_name, default)
clear_cache!
column = column_for(table_name, column_name)
+ return unless column
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ execute alter_column_query % "DROP DEFAULT"
+ else
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ end
end
def change_column_null(table_name, column_name, null, default = nil)
@@ -433,9 +483,48 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
+ validate_index_length!(table_name, new_name)
+
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
@@ -485,7 +574,8 @@ module ActiveRecord
# 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, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
[super, *order_columns].join(', ')
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index 60ffd3a114..9a0b80d7d3 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -1,13 +1,61 @@
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
+ # Value Object to hold a schema qualified name.
+ # This is usually the name of a PostgreSQL relation but it can also represent
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
+ # double quoting.
+ class Name # :nodoc:
+ SEPARATOR = "."
+ attr_reader :schema, :identifier
+
+ def initialize(schema, identifier)
+ @schema, @identifier = unquote(schema), unquote(identifier)
+ end
+
+ def to_s
+ parts.join SEPARATOR
+ end
+
+ def quoted
+ if schema
+ PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
+ else
+ PGconn.quote_ident(identifier)
+ end
+ end
+
+ def ==(o)
+ o.class == self.class && o.parts == parts
+ end
+ alias_method :eql?, :==
+
+ def hash
+ parts.hash
+ end
+
+ protected
+ def unquote(part)
+ if part && part.start_with?('"')
+ part[1..-2]
+ else
+ part
+ end
+ end
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+ end
+
module Utils # :nodoc:
extend self
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
- # +schema_name+ is nil if not specified in +name+.
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
+ # extracted from +string+.
+ # +schema+ is nil if not specified in +string+.
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
#
# * <tt>table_name</tt>
# * <tt>"table.name"</tt>
@@ -15,9 +63,13 @@ module ActiveRecord
# * <tt>schema_name."table.name"</tt>
# * <tt>"schema_name".table_name</tt>
# * <tt>"schema.name"."table name"</tt>
- def extract_schema_and_table(name)
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
- [schema, table]
+ def extract_schema_qualified_name(string)
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ if table.nil?
+ table = schema
+ schema = nil
+ end
+ PostgreSQL::Name.new(schema, table)
end
end
end