Skip to content
This repository
Browse code

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
Dickson S. Guedes authored September 05, 2012
5  activerecord/CHANGELOG.md
Source Rendered
@@ -14,6 +14,11 @@
14 14
 
15 15
     *Ian Lesperance*
16 16
 
  17
+*   Allow JSON columns to be created in PostgreSQL and properly encoded/decoded
  18
+    to/from database.
  19
+
  20
+    *Dickson S. Guedes*
  21
+
17 22
 *   Fix time column type casting for invalid time string values to correctly return nil.
18 23
 
19 24
     *Adam Meehan*
1  activerecord/lib/active_record/connection_adapters/column.rb
@@ -124,6 +124,7 @@ def type_cast_code(var_name)
124 124
         when :boolean              then "#{klass}.value_to_boolean(#{var_name})"
125 125
         when :hstore               then "#{klass}.string_to_hstore(#{var_name})"
126 126
         when :inet, :cidr          then "#{klass}.string_to_cidr(#{var_name})"
  127
+        when :json                 then "#{klass}.string_to_json(#{var_name})"
127 128
         else var_name
128 129
         end
129 130
       end
16  activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -37,6 +37,22 @@ def string_to_hstore(string)
37 37
           end
38 38
         end
39 39
 
  40
+        def json_to_string(object)
  41
+          if Hash === object
  42
+            ActiveSupport::JSON.encode(object)
  43
+          else
  44
+            object
  45
+          end
  46
+        end
  47
+
  48
+        def string_to_json(string)
  49
+          if String === string
  50
+            ActiveSupport::JSON.decode(string)
  51
+          else
  52
+            string
  53
+          end
  54
+        end
  55
+
40 56
         def string_to_cidr(string)
41 57
           if string.nil?
42 58
             nil
9  activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -145,6 +145,14 @@ def type_cast(value)
145 145
           end
146 146
         end
147 147
 
  148
+        class Json < Type
  149
+          def type_cast(value)
  150
+            return if value.nil?
  151
+
  152
+            ConnectionAdapters::PostgreSQLColumn.string_to_json value
  153
+          end
  154
+        end
  155
+
148 156
         class TypeMap
149 157
           def initialize
150 158
             @mapping = {}
@@ -244,6 +252,7 @@ def self.registered_type?(name)
244 252
         register_type 'polygon', OID::Identity.new
245 253
         register_type 'circle', OID::Identity.new
246 254
         register_type 'hstore', OID::Hstore.new
  255
+        register_type 'json', OID::Json.new
247 256
 
248 257
         register_type 'cidr', OID::Cidr.new
249 258
         alias_type 'inet', 'cidr'
8  activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -22,6 +22,7 @@ def quote(value, column = nil) #:nodoc:
22 22
           when Hash
23 23
             case column.sql_type
24 24
             when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
  25
+            when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
25 26
             else super
26 27
             end
27 28
           when IPAddr
@@ -66,8 +67,11 @@ def type_cast(value, column)
66 67
             return super unless 'bytea' == column.sql_type
67 68
             { :value => value, :format => 1 }
68 69
           when Hash
69  
-            return super unless 'hstore' == column.sql_type
70  
-            PostgreSQLColumn.hstore_to_string(value)
  70
+            case column.sql_type
  71
+            when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
  72
+            when 'json' then PostgreSQLColumn.json_to_string(value)
  73
+            else super
  74
+            end
71 75
           when IPAddr
72 76
             return super unless ['inet','cidr'].includes? column.sql_type
73 77
             PostgreSQLColumn.cidr_to_string(value)
13  activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -106,6 +106,9 @@ def self.extract_value_from_default(default)
106 106
           # Hstore
107 107
           when /\A'(.*)'::hstore\z/
108 108
             $1
  109
+          # JSON
  110
+          when /\A'(.*)'::json\z/
  111
+            $1
109 112
           # Object identifier types
110 113
           when /\A-?\d+\z/
111 114
             $1
@@ -201,6 +204,9 @@ def simplified_type(field_type)
201 204
           # UUID type
202 205
           when 'uuid'
203 206
             :uuid
  207
+        # JSON type
  208
+        when 'json'
  209
+          :json
204 210
           # Small and big integer types
205 211
           when /^(?:small|big)int$/
206 212
             :integer
@@ -267,6 +273,10 @@ def macaddr(name, options = {})
267 273
         def uuid(name, options = {})
268 274
           column(name, 'uuid', options)
269 275
         end
  276
+
  277
+        def json(name, options = {})
  278
+          column(name, 'json', options)
  279
+        end
270 280
       end
271 281
 
272 282
       ADAPTER_NAME = 'PostgreSQL'
@@ -290,7 +300,8 @@ def uuid(name, options = {})
290 300
         inet:        { name: "inet" },
291 301
         cidr:        { name: "cidr" },
292 302
         macaddr:     { name: "macaddr" },
293  
-        uuid:        { name: "uuid" }
  303
+        uuid:        { name: "uuid" },
  304
+        json:        { name: "json" }
294 305
       }
295 306
 
296 307
       include Quoting
69  activerecord/test/cases/adapters/postgresql/json_test.rb
... ...
@@ -0,0 +1,69 @@
  1
+# encoding: utf-8
  2
+
  3
+require "cases/helper"
  4
+require 'active_record/base'
  5
+require 'active_record/connection_adapters/postgresql_adapter'
  6
+
  7
+class PostgresqlJSONTest < ActiveRecord::TestCase
  8
+  class JsonDataType < ActiveRecord::Base
  9
+    self.table_name = 'json_data_type'
  10
+  end
  11
+
  12
+  def setup
  13
+    @connection = ActiveRecord::Base.connection
  14
+    begin
  15
+      @connection.create_table('json_data_type') do |t|
  16
+        t.json 'payload', :default => {}
  17
+      end
  18
+    rescue ActiveRecord::StatementInvalid
  19
+      return skip "do not test on PG without json"
  20
+    end
  21
+    @column = JsonDataType.columns.find { |c| c.name == 'payload' }
  22
+  end
  23
+
  24
+  def teardown
  25
+    @connection.execute 'drop table if exists json_data_type'
  26
+  end
  27
+
  28
+  def test_column
  29
+    assert_equal :json, @column.type
  30
+  end
  31
+
  32
+  def test_type_cast_json
  33
+    assert @column
  34
+
  35
+    data = "{\"a_key\":\"a_value\"}"
  36
+    hash = @column.class.string_to_json data
  37
+    assert_equal({'a_key' => 'a_value'}, hash)
  38
+    assert_equal({'a_key' => 'a_value'}, @column.type_cast(data))
  39
+
  40
+    assert_equal({}, @column.type_cast("{}"))
  41
+    assert_equal({'key'=>nil}, @column.type_cast('{"key": null}'))
  42
+    assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
  43
+  end
  44
+
  45
+  def test_rewrite
  46
+    @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
  47
+    x = JsonDataType.first
  48
+    x.payload = { '"a\'' => 'b' }
  49
+    assert x.save!
  50
+  end
  51
+
  52
+  def test_select
  53
+    @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
  54
+    x = JsonDataType.first
  55
+    assert_equal({'k' => 'v'}, x.payload)
  56
+  end
  57
+
  58
+  def test_select_multikey
  59
+    @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|
  60
+    x = JsonDataType.first
  61
+    assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload)
  62
+  end
  63
+
  64
+  def test_null_json
  65
+    @connection.execute %q|insert into json_data_type (payload) VALUES(null)|
  66
+    x = JsonDataType.first
  67
+    assert_equal(nil, x.payload)
  68
+  end
  69
+end
7  activerecord/test/cases/schema_dumper_test.rb
@@ -236,6 +236,13 @@ def test_schema_dump_includes_xml_shorthand_definition
236 236
       end
237 237
     end
238 238
 
  239
+    def test_schema_dump_includes_json_shorthand_definition
  240
+      output = standard_dump
  241
+      if %r{create_table "postgresql_json_data_type"} =~ output
  242
+        assert_match %r|t.json "json_data", :default => {}|, output
  243
+      end
  244
+    end
  245
+
239 246
     def test_schema_dump_includes_inet_shorthand_definition
240 247
       output = standard_dump
241 248
       if %r{create_table "postgresql_network_address"} =~ output
11  activerecord/test/schema/postgresql_specific_schema.rb
... ...
@@ -1,7 +1,7 @@
1 1
 ActiveRecord::Schema.define do
2 2
 
3 3
   %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids
4  
-      postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
  4
+      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|
5 5
     execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
6 6
   end
7 7
 
@@ -82,6 +82,15 @@
82 82
 _SQL
83 83
   end
84 84
 
  85
+  if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
  86
+  execute <<_SQL
  87
+  CREATE TABLE postgresql_json_data_type (
  88
+    id SERIAL PRIMARY KEY,
  89
+    json_data json default '{}'::json
  90
+  );
  91
+_SQL
  92
+  end
  93
+
85 94
   execute <<_SQL
86 95
   CREATE TABLE postgresql_moneys (
87 96
     id SERIAL PRIMARY KEY,

0 notes on commit 3b516b5

Please sign in to comment.
Something went wrong with that request. Please try again.