Skip to content
This repository
Browse code

PostgreSQL: XML datatype support

[#1874 state:committed]

Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
  • Loading branch information...
commit 0c391b46fb39b697bbae1493caade23e2ddbd8a6 1 parent 9c1bac0
Leonardo Borges authored August 09, 2009 jeremy committed August 09, 2009
2  activerecord/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *Edge*
2 2
 
  3
+* PostgreSQL: XML datatype support.  #1874 [Leonardo Borges]
  4
+
3 5
 * quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 [Geoff Buesing]
4 6
 
5 7
 * SQLite: drop support for 'dbfile' option in favor of 'database.'  #2363 [Paul Hinze, Jeremy Kemper]
15  activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -315,6 +315,20 @@ def initialize(base)
315 315
         @base = base
316 316
       end
317 317
 
  318
+      #Handles non supported datatypes - e.g. XML
  319
+      def method_missing(symbol, *args)
  320
+        if symbol.to_s == 'xml'
  321
+          xml_column_fallback(args)
  322
+        end
  323
+      end
  324
+
  325
+      def xml_column_fallback(*args)
  326
+        case @base.adapter_name.downcase
  327
+          when 'sqlite', 'mysql'
  328
+            options = args.extract_options!
  329
+            column(args[0], :text, options)
  330
+          end
  331
+        end
318 332
       # Appends a primary key definition to the table definition.
319 333
       # Can be called multiple times, but this is probably not a good idea.
320 334
       def primary_key(name)
@@ -705,3 +719,4 @@ def native
705 719
 
706 720
   end
707 721
 end
  722
+
34  activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -40,6 +40,12 @@ def self.postgresql_connection(config) # :nodoc:
40 40
   end
41 41
 
42 42
   module ConnectionAdapters
  43
+    class TableDefinition
  44
+      def xml(*args)
  45
+        options = args.extract_options!
  46
+        column(args[0], 'xml', options)
  47
+      end
  48
+    end
43 49
     # PostgreSQL-specific extensions to column definitions in a table.
44 50
     class PostgreSQLColumn < Column #:nodoc:
45 51
       # Instantiates a new PostgreSQL column definition in a table.
@@ -68,7 +74,7 @@ def extract_precision(sql_type)
68 74
           # depending on the server specifics
69 75
           super
70 76
         end
71  
-  
  77
+
72 78
         # Maps PostgreSQL-specific data types to logical Rails types.
73 79
         def simplified_type(field_type)
74 80
           case field_type
@@ -100,10 +106,10 @@ def simplified_type(field_type)
100 106
               :string
101 107
             # XML type
102 108
             when /^xml$/
103  
-              :string
  109
+              :xml
104 110
             # Arrays
105 111
             when /^\D+\[\]$/
106  
-              :string              
  112
+              :string
107 113
             # Object identifier types
108 114
             when /^oid$/
109 115
               :integer
@@ -112,7 +118,7 @@ def simplified_type(field_type)
112 118
               super
113 119
           end
114 120
         end
115  
-  
  121
+
116 122
         # Extracts the value from a PostgreSQL column default definition.
117 123
         def self.extract_value_from_default(default)
118 124
           case default
@@ -195,7 +201,8 @@ class PostgreSQLAdapter < AbstractAdapter
195 201
         :time        => { :name => "time" },
196 202
         :date        => { :name => "date" },
197 203
         :binary      => { :name => "bytea" },
198  
-        :boolean     => { :name => "boolean" }
  204
+        :boolean     => { :name => "boolean" },
  205
+        :xml         => { :name => "xml" }
199 206
       }
200 207
 
201 208
       # Returns 'PostgreSQL' as adapter name for identification purposes.
@@ -278,7 +285,7 @@ def supports_insert_with_returning?
278 285
       def supports_ddl_transactions?
279 286
         true
280 287
       end
281  
-      
  288
+
282 289
       def supports_savepoints?
283 290
         true
284 291
       end
@@ -370,7 +377,7 @@ def quote(value, column = nil) #:nodoc:
370 377
         if value.kind_of?(String) && column && column.type == :binary
371 378
           "#{quoted_string_prefix}'#{escape_bytea(value)}'"
372 379
         elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
373  
-          "xml '#{quote_string(value)}'"
  380
+          "xml E'#{quote_string(value)}'"
374 381
         elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
375 382
           # Not truly string input, so doesn't require (or allow) escape string syntax.
376 383
           "'#{value.to_s}'"
@@ -569,7 +576,7 @@ def commit_db_transaction
569 576
       def rollback_db_transaction
570 577
         execute "ROLLBACK"
571 578
       end
572  
-      
  579
+
573 580
       if defined?(PGconn::PQTRANS_IDLE)
574 581
         # The ruby-pg driver supports inspecting the transaction status,
575 582
         # while the ruby-postgres driver does not.
@@ -920,18 +927,18 @@ def distinct(columns, order_by) #:nodoc:
920 927
         sql = "DISTINCT ON (#{columns}) #{columns}, "
921 928
         sql << order_columns * ', '
922 929
       end
923  
-      
  930
+
924 931
       # Returns an ORDER BY clause for the passed order option.
925  
-      # 
  932
+      #
926 933
       # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
927 934
       # by wrapping the +sql+ string as a sub-select and ordering in that query.
928 935
       def add_order_by_for_association_limiting!(sql, options) #:nodoc:
929 936
         return sql if options[:order].blank?
930  
-        
  937
+
931 938
         order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
932 939
         order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
933 940
         order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
934  
-        
  941
+
935 942
         sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
936 943
       end
937 944
 
@@ -1055,7 +1062,7 @@ def select_raw(sql, name = nil)
1055 1062
                 if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID
1056 1063
                   # Because money output is formatted according to the locale, there are two
1057 1064
                   # cases to consider (note the decimal separators):
1058  
-                  #  (1) $12,345,678.12        
  1065
+                  #  (1) $12,345,678.12
1059 1066
                   #  (2) $12.345.678,12
1060 1067
                   case column = row[cell_index]
1061 1068
                     when /^-?\D+[\d,]+\.\d{2}$/  # (1)
@@ -1115,3 +1122,4 @@ def extract_pg_identifier_from_name(name)
1115 1122
     end
1116 1123
   end
1117 1124
 end
  1125
+
28  activerecord/test/cases/migration_test.rb
@@ -396,7 +396,7 @@ def test_add_column_with_precision_and_scale
396 396
       assert_equal 9, wealth_column.precision
397 397
       assert_equal 7, wealth_column.scale
398 398
     end
399  
-    
  399
+
400 400
     def test_native_types
401 401
       Person.delete_all
402 402
       Person.connection.add_column "people", "last_name", :string
@@ -975,9 +975,9 @@ def test_migrator_one_up
975 975
 
976 976
     def test_migrator_one_down
977 977
       ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid")
978  
-    
  978
+
979 979
       ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 1)
980  
-    
  980
+
981 981
       Person.reset_column_information
982 982
       assert Person.column_methods_hash.include?(:last_name)
983 983
       assert !Reminder.table_exists?
@@ -1118,20 +1118,20 @@ def test_migrator_going_down_due_to_version_target
1118 1118
       assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
1119 1119
       assert_equal "hello world", Reminder.find(:first).content
1120 1120
     end
1121  
-    
  1121
+
1122 1122
     def test_migrator_rollback
1123 1123
       ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
1124 1124
       assert_equal(3, ActiveRecord::Migrator.current_version)
1125  
-      
  1125
+
1126 1126
       ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
1127 1127
       assert_equal(2, ActiveRecord::Migrator.current_version)
1128  
-      
  1128
+
1129 1129
       ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
1130 1130
       assert_equal(1, ActiveRecord::Migrator.current_version)
1131  
-      
  1131
+
1132 1132
       ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
1133 1133
       assert_equal(0, ActiveRecord::Migrator.current_version)
1134  
-      
  1134
+
1135 1135
       ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
1136 1136
       assert_equal(0, ActiveRecord::Migrator.current_version)
1137 1137
     end
@@ -1294,7 +1294,7 @@ def with_env_tz(new_tz = 'US/Eastern')
1294 1294
       end
1295 1295
 
1296 1296
   end
1297  
-  
  1297
+
1298 1298
   class SexyMigrationsTest < ActiveRecord::TestCase
1299 1299
     def test_references_column_type_adds_id
1300 1300
       with_new_table do |t|
@@ -1350,6 +1350,15 @@ def test_string_creates_string_column
1350 1350
       end
1351 1351
     end
1352 1352
 
  1353
+    if current_adapter?(:PostgreSQLAdapter)
  1354
+      def test_xml_creates_xml_column
  1355
+        with_new_table do |t|
  1356
+          t.expects(:column).with(:data, 'xml', {})
  1357
+          t.xml :data
  1358
+        end
  1359
+      end
  1360
+    end
  1361
+
1353 1362
     protected
1354 1363
     def with_new_table
1355 1364
       Person.connection.create_table :delete_me, :force => true do |t|
@@ -1567,3 +1576,4 @@ def with_change_table
1567 1576
     end
1568 1577
   end
1569 1578
 end
  1579
+
12  activerecord/test/cases/schema_dumper_test.rb
@@ -161,7 +161,7 @@ def test_schema_dumps_index_columns_in_right_order
161 161
     index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
162 162
     assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition
163 163
   end
164  
-  
  164
+
165 165
   def test_schema_dump_should_honor_nonstandard_primary_keys
166 166
     output = standard_dump
167 167
     match = output.match(%r{create_table "movies"(.*)do})
@@ -196,6 +196,15 @@ def test_schema_dump_includes_decimal_options
196 196
     assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 2.78}, output
197 197
   end
198 198
 
  199
+  if current_adapter?(:PostgreSQLAdapter)
  200
+    def test_schema_dump_includes_xml_shorthand_definition
  201
+      output = standard_dump
  202
+      if %r{create_table "postgresql_xml_data_type"} =~ output
  203
+        assert_match %r{t.xml "data"}, output
  204
+      end
  205
+    end
  206
+  end
  207
+
199 208
   def test_schema_dump_keeps_large_precision_integer_columns_as_decimal
200 209
     output = standard_dump
201 210
     # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
@@ -214,3 +223,4 @@ def test_schema_dump_keeps_id_column_when_id_is_false_and_id_column_added
214 223
     assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved"
215 224
   end
216 225
 end
  226
+
15  activerecord/test/schema/postgresql_specific_schema.rb
... ...
@@ -1,7 +1,7 @@
1 1
 ActiveRecord::Schema.define do
2 2
 
3 3
   %w(postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings
4  
-      postgresql_oids defaults geometrics).each do |table_name|
  4
+      postgresql_oids postgresql_xml_data_type defaults geometrics).each do |table_name|
5 5
     execute "DROP TABLE  IF EXISTS #{quote_table_name table_name}"
6 6
   end
7 7
 
@@ -100,4 +100,15 @@
100 100
     obj_id OID
101 101
   );
102 102
 _SQL
103  
-end
  103
+
  104
+  begin
  105
+    execute <<_SQL
  106
+    CREATE TABLE postgresql_xml_data_type (
  107
+    id SERIAL PRIMARY KEY,
  108
+    data xml
  109
+    );
  110
+_SQL
  111
+rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
  112
+  end
  113
+end
  114
+

1 note on commit 0c391b4

Roderick van Domburg

Cool! On line 380 of postgresql_adapter.rb, the "E" quoting syntax should probably be replaced with #{quoted_string_prefix} for consistency.

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