activerecord-postgis-adapter 2.2.2 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module PostGISAdapter
4
+ module SchemaStatements
5
+ # override
6
+ # pass table_name to #new_column
7
+ def columns(table_name)
8
+ # Limit, precision, and scale are all handled by the superclass.
9
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
10
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
11
+ default_value = extract_value_from_default(oid, default)
12
+ default_function = extract_default_function(default_value, default)
13
+ new_column(table_name, column_name, default_value, oid, type, notnull == 'f', default_function)
14
+ end
15
+ end
16
+
17
+ # override
18
+ def new_column(table_name, column_name, default, cast_type, sql_type = nil, null = true, default_function = nil)
19
+ # JDBC gets true/false in Rails 4, where other platforms get 't'/'f' strings.
20
+ if null.is_a?(String)
21
+ null = (null == 't')
22
+ end
23
+
24
+ column_info = spatial_column_info(table_name).get(column_name, sql_type)
25
+
26
+ SpatialColumn.new(@rgeo_factory_settings,
27
+ table_name,
28
+ column_name,
29
+ default,
30
+ cast_type,
31
+ sql_type,
32
+ null,
33
+ column_info)
34
+ end
35
+
36
+ # override
37
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L533
38
+ #
39
+ # returns Postgresql sql type string
40
+ # examples:
41
+ # "geometry(Point,4326)"
42
+ # "geography(Point,4326)"
43
+ #
44
+ # note: type alone is not enough to detect the sql type,
45
+ # so `limit` is used to pass the additional information. :(
46
+ #
47
+ # type_to_sql(:geography, "Point,4326")
48
+ # => "geography(Point,4326)"
49
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
50
+ case type
51
+ when :geometry, :geography
52
+ "#{ type.to_s }(#{ limit })"
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ # override
59
+ def native_database_types
60
+ # Add spatial types
61
+ super.merge(
62
+ geography: "geography",
63
+ geometry: "geometry",
64
+ geometry_collection: "geometry_collection",
65
+ line_string: "line_string",
66
+ multi_line_string: "multi_line_string",
67
+ multi_point: "multi_point",
68
+ multi_polygon: "multi_polygon",
69
+ spatial: "geometry",
70
+ st_point: "st_point",
71
+ st_polygon: "st_polygon",
72
+ )
73
+ end
74
+
75
+ # override
76
+ def create_table_definition(name, temporary, options, as = nil)
77
+ PostGISAdapter::TableDefinition.new(native_database_types, name, temporary, options, as, self)
78
+ end
79
+
80
+ # memoize hash of column infos for tables
81
+ def spatial_column_info(table_name)
82
+ @spatial_column_info ||= {}
83
+ @spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
84
+ end
85
+
86
+ def initialize_type_map(map)
87
+ super
88
+
89
+ %w(
90
+ geography
91
+ geometry
92
+ geometry_collection
93
+ line_string
94
+ multi_line_string
95
+ multi_point
96
+ multi_polygon
97
+ st_point
98
+ st_polygon
99
+ )
100
+ .each do |geo_type|
101
+ map.register_type(geo_type) do |oid, _, sql_type|
102
+ OID::Spatial.new(oid, sql_type)
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
@@ -3,164 +3,74 @@ module ActiveRecord # :nodoc:
3
3
  module PostGISAdapter # :nodoc:
4
4
  class SpatialColumn < ConnectionAdapters::PostgreSQLColumn # :nodoc:
5
5
 
6
- def initialize(factory_settings, table_name, name, default, oid_type, sql_type = nil, null = true, opts = nil)
6
+ # sql_type examples:
7
+ # "Geometry(Point,4326)"
8
+ # "Geography(Point,4326)"
9
+ # cast_type example classes:
10
+ # OID::Spatial
11
+ # OID::Integer
12
+ def initialize(factory_settings, table_name, name, default, cast_type, sql_type = nil, null = true, opts = nil)
7
13
  @factory_settings = factory_settings
8
14
  @table_name = table_name
9
- @geographic = !!(sql_type =~ /geography/i)
15
+ @geographic = !!(sql_type =~ /geography\(/i)
10
16
  if opts
11
17
  # This case comes from an entry in the geometry_columns table
12
- @geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name(opts[:type]) || ::RGeo::Feature::Geometry
18
+ set_geometric_type_from_name(opts[:type])
13
19
  @srid = opts[:srid].to_i
14
20
  @has_z = !!opts[:has_z]
15
21
  @has_m = !!opts[:has_m]
16
22
  elsif @geographic
17
23
  # Geographic type information is embedded in the SQL type
18
- @geometric_type = ::RGeo::Feature::Geometry
19
24
  @srid = 4326
20
25
  @has_z = @has_m = false
21
- if sql_type =~ /geography\((.*)\)$/i
22
- params = $1.split(',')
23
- if params.size >= 2
24
- if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
25
- @has_z = $2.length > 0
26
- @has_m = $3.length > 0
27
- @geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name($1)
28
- end
29
- if params.last =~ /(\d+)/
30
- @srid = $1.to_i
31
- end
32
- end
33
- end
26
+ build_from_sql_type(sql_type)
34
27
  elsif sql_type =~ /geography|geometry|point|linestring|polygon/i
35
- # Just in case there is a geometry column with no geometry_columns entry.
36
- @geometric_type = ::RGeo::Feature::Geometry
37
- @srid = @has_z = @has_m = nil
38
- else
39
- # Non-spatial column
40
- @geometric_type = @has_z = @has_m = @srid = nil
28
+ # A geometry column with no geometry_columns entry.
29
+ # @geometric_type = geo_type_from_sql_type(sql_type)
30
+ build_from_sql_type(sql_type)
41
31
  end
42
- super(name, default, oid_type, sql_type, null)
32
+ super(name, default, cast_type, sql_type, null)
43
33
  if spatial?
44
34
  if @srid
45
- @limit = { srid: @srid, type: @geometric_type.type_name.underscore }
35
+ @limit = { srid: @srid, type: geometric_type.type_name.underscore }
46
36
  @limit[:has_z] = true if @has_z
47
37
  @limit[:has_m] = true if @has_m
48
38
  @limit[:geographic] = true if @geographic
49
- else
50
- @limit = { no_constraints: true }
51
39
  end
52
40
  end
53
41
  end
54
42
 
55
- attr_reader :geographic
56
- attr_reader :srid
57
- attr_reader :geometric_type
58
- attr_reader :has_z
59
- attr_reader :has_m
43
+ attr_reader :geographic,
44
+ :geometric_type,
45
+ :has_m,
46
+ :has_z,
47
+ :limit, # override
48
+ :srid
60
49
 
61
- alias_method :geographic?, :geographic
62
- alias_method :has_z?, :has_z
63
- alias_method :has_m?, :has_m
50
+ alias :geographic? :geographic
51
+ alias :has_z? :has_z
52
+ alias :has_m? :has_m
64
53
 
65
54
  def spatial?
66
- type == :spatial || type == :geography
55
+ cast_type.respond_to?(:spatial?) && cast_type.spatial?
67
56
  end
68
57
 
58
+ # TODO: delete - unused?
69
59
  def has_spatial_constraints?
70
- !@srid.nil?
71
- end
72
-
73
- def klass
74
- spatial? ? ::RGeo::Feature::Geometry : super
75
- end
76
-
77
- def type_cast(value)
78
- if spatial?
79
- SpatialColumn.convert_to_geometry(value, @factory_settings, @table_name, name,
80
- @geographic, @srid, @has_z, @has_m)
81
- else
82
- super
83
- end
60
+ !!@srid
84
61
  end
85
62
 
86
63
  private
87
64
 
88
- def simplified_type(sql_type)
89
- sql_type =~ /geography|geometry|point|linestring|polygon/i ? :spatial : super
90
- end
91
-
92
- def self.convert_to_geometry(input, factory_settings, table_name, column, geographic, srid, has_z, has_m)
93
- if srid
94
- constraints = {
95
- geographic: geographic,
96
- has_z_coordinate: has_z,
97
- has_m_coordinate: has_m,
98
- srid: srid
99
- }
100
- else
101
- constraints = nil
102
- end
103
- if ::RGeo::Feature::Geometry === input
104
- factory = factory_settings.get_column_factory(table_name, column, constraints)
105
- ::RGeo::Feature.cast(input, factory) rescue nil
106
- elsif input.respond_to?(:to_str)
107
- input = input.to_str
108
- if input.length == 0
109
- nil
110
- else
111
- factory = factory_settings.get_column_factory(table_name, column, constraints)
112
- marker = input[0,1]
113
- if marker == "\x00" || marker == "\x01" || input[0,4] =~ /[0-9a-fA-F]{4}/
114
- ::RGeo::WKRep::WKBParser.new(factory, support_ewkb: true).parse(input) rescue nil
115
- else
116
- ::RGeo::WKRep::WKTParser.new(factory, support_ewkt: true).parse(input) rescue nil
117
- end
118
- end
119
- else
120
- nil
121
- end
122
- end
123
-
124
- end
125
-
126
- # Register spatial types with the postgres OID mechanism
127
- # so we can recognize custom columns coming from the database.
128
- class SpatialOID < PostgreSQLAdapter::OID::Type # :nodoc:
129
-
130
- def initialize(factory_generator)
131
- @factory_generator = factory_generator
65
+ def set_geometric_type_from_name(name)
66
+ @geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
132
67
  end
133
68
 
134
- def type_cast(value)
135
- return if value.nil?
136
- ::RGeo::WKRep::WKBParser.new(@factory_generator, support_ewkb: true).parse(value) rescue nil
137
- end
138
-
139
- end
140
-
141
- PostgreSQLAdapter::OID.register_type('geometry', SpatialOID.new(nil))
142
- PostgreSQLAdapter::OID.register_type('geography', SpatialOID.new(::RGeo::Geographic.method(:spherical_factory)))
143
-
144
- # This is a hack to ActiveRecord::ModelSchema. We have to "decorate" the decorate_columns
145
- # method to apply class-specific customizations to spatial type casting.
146
- module DecorateColumnsModification # :nodoc:
147
-
148
- def decorate_columns(columns_hash)
149
- columns_hash = super(columns_hash)
150
- return unless columns_hash
151
- canonical_columns_ = self.columns_hash
152
- columns_hash.each do |name, col|
153
- if col.is_a?(SpatialOID) && (canonical = canonical_columns_[name]) && canonical.spatial?
154
- columns_hash[name] = canonical
155
- end
156
- end
157
- columns_hash
69
+ def build_from_sql_type(sql_type)
70
+ geo_type, @srid, @has_z, @has_m = OID::Spatial.parse_sql_type(sql_type)
71
+ set_geometric_type_from_name(geo_type)
158
72
  end
159
-
160
73
  end
161
-
162
- ::ActiveRecord::Base.extend(DecorateColumnsModification)
163
-
164
74
  end
165
75
  end
166
76
  end
@@ -30,9 +30,9 @@ module ActiveRecord # :nodoc:
30
30
  result
31
31
  end
32
32
 
33
- # will not query the database for non-spatial columns/tables
33
+ # do not query the database for non-spatial columns/tables
34
34
  def get(column_name, type)
35
- return nil unless type =~ /geometry/i
35
+ return unless MainAdapter.spatial_column_options(type.to_sym)
36
36
  @spatial_column_info ||= all
37
37
  @spatial_column_info[column_name]
38
38
  end
@@ -1,120 +1,170 @@
1
1
  module ActiveRecord # :nodoc:
2
2
  module ConnectionAdapters # :nodoc:
3
3
  module PostGISAdapter # :nodoc:
4
- class TableDefinition < ConnectionAdapters::PostgreSQLAdapter::TableDefinition # :nodoc:
4
+ class TableDefinition < PostgreSQL::TableDefinition # :nodoc:
5
5
 
6
- if ActiveRecord::VERSION::STRING > '4.1'
7
- def initialize(types, name, temporary, options, as, base)
8
- @base = base
9
- @spatial_columns_hash = {}
10
- super(types, name, temporary, options, as)
11
- end
12
- else
13
- def initialize(types, name, temporary, options, base)
14
- @base = base
15
- @spatial_columns_hash = {}
16
- super(types, name, temporary, options)
17
- end
6
+ def initialize(types, name, temporary, options, as, adapter)
7
+ @adapter = adapter
8
+ @spatial_columns_hash = {}
9
+ super(types, name, temporary, options, as)
18
10
  end
19
11
 
20
- def column(name, type, options={})
21
- if (info = @base.spatial_column_constructor(type.to_sym))
22
- type = options[:type] || info[:type] || type
23
- if type.to_s == 'geometry' && (options[:no_constraints] || options[:limit].is_a?(::Hash) && options[:limit][:no_constraints])
24
- options.delete(:limit)
25
- else
26
- options[:type] = type
27
- type = :spatial
28
- end
29
- end
30
- if type == :spatial
12
+ # super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L320
13
+ def new_column_definition(name, type, options)
14
+ if (info = MainAdapter.spatial_column_options(type.to_sym))
15
+ geo_type = ColumnDefinition.geo_type(options[:type] || type || info[:type])
16
+ base_type = info[:type] || (options[:geographic] ? :geography : :geometry)
17
+
18
+ # puts name.dup << " - " << type.to_s << " - " << options.to_s << " :: " << geo_type.to_s << " - " << base_type.to_s
19
+
31
20
  if (limit = options.delete(:limit))
32
21
  options.merge!(limit) if limit.is_a?(::Hash)
33
22
  end
34
23
  if options[:geographic]
35
- type = :geography
36
- spatial_type = (options[:type] || 'geometry').to_s.upcase.gsub('_', '')
37
- spatial_type << 'Z' if options[:has_z]
38
- spatial_type << 'M' if options[:has_m]
39
- options[:limit] = "#{spatial_type},#{options[:srid] || 4326}"
40
- end
41
- name = name.to_s
42
- if primary_key_column_name == name
43
- raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
24
+ options[:limit] = ColumnDefinition.options_to_limit(geo_type, options)
44
25
  end
45
- column = new_column_definition(name, type, options)
46
- column.set_spatial_type(options[:type])
47
- column.set_geographic(options[:geographic])
48
- column.set_srid(options[:srid])
49
- column.set_has_z(options[:has_z])
50
- column.set_has_m(options[:has_m])
26
+ column = super(name, base_type, options)
27
+ column.spatial_type = geo_type
28
+ column.geographic = options[:geographic]
29
+ column.srid = options[:srid]
30
+ column.has_z = options[:has_z]
31
+ column.has_m = options[:has_m]
51
32
  (column.geographic? ? @columns_hash : @spatial_columns_hash)[name] = column
52
33
  else
53
- super(name, type, options)
34
+ column = super(name, type, options)
54
35
  end
55
- self
36
+
37
+ column
56
38
  end
57
39
 
40
+ def non_geographic_spatial_columns
41
+ @spatial_columns_hash.values
42
+ end
43
+
44
+ def spatial(name, options = {})
45
+ raise "You must set a type. For example: 't.spatial type: :st_point'" unless options[:type]
46
+ column(name, options[:type], options)
47
+ end
48
+
49
+ def geography(name, options = {})
50
+ column(name, :geography, options)
51
+ end
52
+
53
+ def geometry(name, options = {})
54
+ column(name, :geometry, options)
55
+ end
56
+
57
+ def geometry_collection(name, options = {})
58
+ column(name, :geometry_collection, options)
59
+ end
60
+
61
+ def line_string(name, options = {})
62
+ column(name, :line_string, options)
63
+ end
64
+
65
+ def multi_line_string(name, options = {})
66
+ column(name, :multi_line_string, options)
67
+ end
68
+
69
+ def multi_point(name, options = {})
70
+ column(name, :multi_point, options)
71
+ end
72
+
73
+ def multi_polygon(name, options = {})
74
+ column(name, :multi_polygon, options)
75
+ end
76
+
77
+ def st_point(name, options = {})
78
+ column(name, :st_point, options)
79
+ end
80
+
81
+ def st_polygon(name, options = {})
82
+ column(name, :st_polygon, options)
83
+ end
84
+
85
+ private
86
+
58
87
  def create_column_definition(name, type)
59
- if type == :spatial || type == :geography
88
+ if MainAdapter.spatial_column_options(type.to_sym)
60
89
  PostGISAdapter::ColumnDefinition.new(name, type)
61
90
  else
62
91
  super
63
92
  end
64
93
  end
94
+ end
65
95
 
66
- def non_geographic_spatial_columns
67
- @spatial_columns_hash.values
96
+ class ColumnDefinition < PostgreSQL::ColumnDefinition
97
+ # needs to accept the spatial type? or figure out from limit ?
98
+
99
+ def self.options_to_limit(type, options = {})
100
+ spatial_type = geo_type(type)
101
+ spatial_type << "Z" if options[:has_z]
102
+ spatial_type << "M" if options[:has_m]
103
+ spatial_type << ",#{ options[:srid] || 4326 }"
104
+ spatial_type
68
105
  end
69
106
 
70
- end
107
+ # limit is how column options are passed to #type_to_sql
108
+ # returns: "Point,4326"
109
+ def limit
110
+ "".tap do |value|
111
+ value << self.class.geo_type(spatial_type)
112
+ value << "Z" if has_z?
113
+ value << "M" if has_m?
114
+ value << ",#{ srid }"
115
+ end
116
+ end
71
117
 
72
- class ColumnDefinition < ConnectionAdapters::ColumnDefinition # :nodoc:
118
+ def self.geo_type(type = "GEOMETRY")
119
+ g_type = type.to_s.gsub("_", "").upcase
120
+ return "POINT" if g_type == "STPOINT"
121
+ return "POLYGON" if g_type == "STPOLYGON"
122
+ g_type
123
+ end
73
124
 
74
125
  def spatial_type
75
126
  @spatial_type
76
127
  end
77
128
 
129
+ def spatial_type=(value)
130
+ @spatial_type = value.to_s
131
+ end
132
+
78
133
  def geographic?
79
134
  @geographic
80
135
  end
81
136
 
137
+ def geographic=(value)
138
+ @geographic = !!value
139
+ end
140
+
82
141
  def srid
83
142
  if @srid
84
143
  @srid.to_i
85
144
  else
86
- geographic? ? 4326 : PostGISAdapter::DEFAULT_SRID
145
+ geographic? ? 4326 : PostGISAdapter::MainAdapter::DEFAULT_SRID
87
146
  end
88
147
  end
89
148
 
90
- def has_z?
91
- @has_z
92
- end
93
-
94
- def has_m?
95
- @has_m
96
- end
97
-
98
- def set_geographic(value)
99
- @geographic = !!value
149
+ def srid=(value)
150
+ @srid = value
100
151
  end
101
152
 
102
- def set_spatial_type(value)
103
- @spatial_type = value.to_s
153
+ def has_z?
154
+ @has_z
104
155
  end
105
156
 
106
- def set_srid(value)
107
- @srid = value
157
+ def has_z=(value)
158
+ @has_z = !!value
108
159
  end
109
160
 
110
- def set_has_z(value)
111
- @has_z = !!value
161
+ def has_m?
162
+ @has_m
112
163
  end
113
164
 
114
- def set_has_m(value)
165
+ def has_m=(value)
115
166
  @has_m = !!value
116
167
  end
117
-
118
168
  end
119
169
 
120
170
  end