activerecord-postgis-adapter 2.2.2 → 3.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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