Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

ActiveRecord support to PostgreSQL 9.2 JSON type

This implements the support to encode/decode JSON
data to/from database and creating columns of type
JSON using a native type [1] supported by PostgreSQL
from version 9.2.

[1] http://www.postgresql.org/docs/9.2/static/datatype-json.html
  • Loading branch information...
commit 3b516b5beb79f7e8c1fdd123e7d5a03c00349cdf 1 parent ddaeaef
@guedes guedes authored
View
5 activerecord/CHANGELOG.md
@@ -14,6 +14,11 @@
*Ian Lesperance*
+* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded
+ to/from database.
+
+ *Dickson S. Guedes*
+
* Fix time column type casting for invalid time string values to correctly return nil.
*Adam Meehan*
View
1  activerecord/lib/active_record/connection_adapters/column.rb
@@ -124,6 +124,7 @@ def type_cast_code(var_name)
when :boolean then "#{klass}.value_to_boolean(#{var_name})"
when :hstore then "#{klass}.string_to_hstore(#{var_name})"
when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
+ when :json then "#{klass}.string_to_json(#{var_name})"
else var_name
end
end
View
16 activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -37,6 +37,22 @@ def string_to_hstore(string)
end
end
+ def json_to_string(object)
+ if Hash === object
+ ActiveSupport::JSON.encode(object)
+ else
+ object
+ end
+ end
+
+ def string_to_json(string)
+ if String === string
+ ActiveSupport::JSON.decode(string)
+ else
+ string
+ end
+ end
+
def string_to_cidr(string)
if string.nil?
nil
View
9 activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -145,6 +145,14 @@ def type_cast(value)
end
end
+ class Json < Type
+ def type_cast(value)
+ return if value.nil?
+
+ ConnectionAdapters::PostgreSQLColumn.string_to_json value
+ end
+ end
+
class TypeMap
def initialize
@mapping = {}
@@ -244,6 +252,7 @@ def self.registered_type?(name)
register_type 'polygon', OID::Identity.new
register_type 'circle', OID::Identity.new
register_type 'hstore', OID::Hstore.new
+ register_type 'json', OID::Json.new
register_type 'cidr', OID::Cidr.new
alias_type 'inet', 'cidr'
View
8 activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -22,6 +22,7 @@ def quote(value, column = nil) #:nodoc:
when Hash
case column.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
@@ -66,8 +67,11 @@ def type_cast(value, column)
return super unless 'bytea' == column.sql_type
{ :value => value, :format => 1 }
when Hash
- return super unless 'hstore' == column.sql_type
- PostgreSQLColumn.hstore_to_string(value)
+ case column.sql_type
+ when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
+ when 'json' then PostgreSQLColumn.json_to_string(value)
+ else super
+ end
when IPAddr
return super unless ['inet','cidr'].includes? column.sql_type
PostgreSQLColumn.cidr_to_string(value)
View
13 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -106,6 +106,9 @@ def self.extract_value_from_default(default)
# Hstore
when /\A'(.*)'::hstore\z/
$1
+ # JSON
+ when /\A'(.*)'::json\z/
+ $1
# Object identifier types
when /\A-?\d+\z/
$1
@@ -201,6 +204,9 @@ def simplified_type(field_type)
# UUID type
when 'uuid'
:uuid
+ # JSON type
+ when 'json'
+ :json
# Small and big integer types
when /^(?:small|big)int$/
:integer
@@ -267,6 +273,10 @@ def macaddr(name, options = {})
def uuid(name, options = {})
column(name, 'uuid', options)
end
+
+ def json(name, options = {})
+ column(name, 'json', options)
+ end
end
ADAPTER_NAME = 'PostgreSQL'
@@ -290,7 +300,8 @@ def uuid(name, options = {})
inet: { name: "inet" },
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
- uuid: { name: "uuid" }
+ uuid: { name: "uuid" },
+ json: { name: "json" }
}
include Quoting
View
69 activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -0,0 +1,69 @@
+# encoding: utf-8
+
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlJSONTest < ActiveRecord::TestCase
+ class JsonDataType < ActiveRecord::Base
+ self.table_name = 'json_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.create_table('json_data_type') do |t|
+ t.json 'payload', :default => {}
+ end
+ rescue ActiveRecord::StatementInvalid
+ return skip "do not test on PG without json"
+ end
+ @column = JsonDataType.columns.find { |c| c.name == 'payload' }
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists json_data_type'
+ end
+
+ def test_column
+ assert_equal :json, @column.type
+ end
+
+ def test_type_cast_json
+ assert @column
+
+ data = "{\"a_key\":\"a_value\"}"
+ hash = @column.class.string_to_json data
+ assert_equal({'a_key' => 'a_value'}, hash)
+ assert_equal({'a_key' => 'a_value'}, @column.type_cast(data))
+
+ assert_equal({}, @column.type_cast("{}"))
+ assert_equal({'key'=>nil}, @column.type_cast('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
+ end
+
+ def test_rewrite
+ @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
+ x = JsonDataType.first
+ x.payload = { '"a\'' => 'b' }
+ assert x.save!
+ end
+
+ def test_select
+ @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
+ x = JsonDataType.first
+ assert_equal({'k' => 'v'}, x.payload)
+ end
+
+ def test_select_multikey
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|
+ x = JsonDataType.first
+ assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload)
+ end
+
+ def test_null_json
+ @connection.execute %q|insert into json_data_type (payload) VALUES(null)|
+ x = JsonDataType.first
+ assert_equal(nil, x.payload)
+ end
+end
View
7 activerecord/test/cases/schema_dumper_test.rb
@@ -236,6 +236,13 @@ def test_schema_dump_includes_xml_shorthand_definition
end
end
+ def test_schema_dump_includes_json_shorthand_definition
+ output = standard_dump
+ if %r{create_table "postgresql_json_data_type"} =~ output
+ assert_match %r|t.json "json_data", :default => {}|, output
+ end
+ end
+
def test_schema_dump_includes_inet_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_network_address"} =~ output
View
11 activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,7 +1,7 @@
ActiveRecord::Schema.define do
%w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids
- postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
+ postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -82,6 +82,15 @@
_SQL
end
+ if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
+ execute <<_SQL
+ CREATE TABLE postgresql_json_data_type (
+ id SERIAL PRIMARY KEY,
+ json_data json default '{}'::json
+ );
+_SQL
+ end
+
execute <<_SQL
CREATE TABLE postgresql_moneys (
id SERIAL PRIMARY KEY,
Please sign in to comment.
Something went wrong with that request. Please try again.