activerecord-postgis 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/activerecord-postgis.gemspec +28 -0
- data/lib/active_record/connection_adapters/postgis/adapter_extensions.rb +85 -0
- data/lib/active_record/connection_adapters/postgis/column_extensions.rb +90 -0
- data/lib/active_record/connection_adapters/postgis/column_methods.rb +61 -0
- data/lib/active_record/connection_adapters/postgis/constants.rb +36 -0
- data/lib/active_record/connection_adapters/postgis/oid/spatial.rb +122 -0
- data/lib/active_record/connection_adapters/postgis/oid/spatial_types.rb +26 -0
- data/lib/active_record/connection_adapters/postgis/quoting.rb +35 -0
- data/lib/active_record/connection_adapters/postgis/schema_dumper.rb +94 -0
- data/lib/active_record/connection_adapters/postgis/schema_statements.rb +136 -0
- data/lib/active_record/connection_adapters/postgis/spatial_column_methods.rb +40 -0
- data/lib/active_record/connection_adapters/postgis/spatial_column_type.rb +173 -0
- data/lib/active_record/connection_adapters/postgis/table_definition.rb +144 -0
- data/lib/active_record/connection_adapters/postgis/type/geography.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/geometry.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/geometry_collection.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/line_string.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_line_string.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_point.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_polygon.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/point.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/polygon.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/spatial.rb +86 -0
- data/lib/active_record/connection_adapters/postgis/version.rb +9 -0
- data/lib/active_record/connection_adapters/postgis.rb +196 -0
- data/lib/activerecord-postgis.rb +12 -0
- data/lib/arel/visitors/postgis.rb +147 -0
- metadata +120 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module Quoting
|
7
|
+
def quote(value)
|
8
|
+
if value.is_a?(RGeo::Feature::Instance)
|
9
|
+
# Convert spatial objects to EWKT format for PostgreSQL
|
10
|
+
if value.srid && value.srid != 0
|
11
|
+
"'SRID=#{value.srid};#{value.as_text}'"
|
12
|
+
else
|
13
|
+
"'#{value.as_text}'"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def type_cast(value)
|
21
|
+
if value.is_a?(RGeo::Feature::Instance)
|
22
|
+
# Convert spatial objects to EWKT string for parameter binding
|
23
|
+
if value.srid && value.srid != 0
|
24
|
+
"SRID=#{value.srid};#{value.as_text}"
|
25
|
+
else
|
26
|
+
value.as_text
|
27
|
+
end
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module SchemaDumper
|
7
|
+
private
|
8
|
+
|
9
|
+
# Tell Rails these are valid column types that should use method syntax
|
10
|
+
def valid_column_spec?(column)
|
11
|
+
return true if column.sql_type =~ /^(geometry|geography)/i
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def column_spec_for_primary_key(column)
|
16
|
+
return super unless column.sql_type =~ /^(geometry|geography)/i
|
17
|
+
spec = { id: column.name }
|
18
|
+
spec[:type] = schema_type(column).to_sym
|
19
|
+
extract_spatial_options(column, spec)
|
20
|
+
spec
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepare_column_options(column)
|
24
|
+
spec = super
|
25
|
+
|
26
|
+
if column.sql_type =~ /^(geometry|geography)/i
|
27
|
+
extract_spatial_options(column, spec)
|
28
|
+
end
|
29
|
+
|
30
|
+
spec
|
31
|
+
end
|
32
|
+
|
33
|
+
# Override to return our spatial types
|
34
|
+
def schema_type(column)
|
35
|
+
if column.sql_type =~ /^(geometry|geography)/i
|
36
|
+
case column.sql_type
|
37
|
+
when /geography\(Point/i, /geometry\(Point/i
|
38
|
+
:st_point
|
39
|
+
when /geography\(LineString/i, /geometry\(LineString/i
|
40
|
+
:st_line_string
|
41
|
+
when /geography\(Polygon/i, /geometry\(Polygon/i
|
42
|
+
:st_polygon
|
43
|
+
when /geography\(MultiPoint/i, /geometry\(MultiPoint/i
|
44
|
+
:st_multi_point
|
45
|
+
when /geography\(MultiLineString/i, /geometry\(MultiLineString/i
|
46
|
+
:st_multi_line_string
|
47
|
+
when /geography\(MultiPolygon/i, /geometry\(MultiPolygon/i
|
48
|
+
:st_multi_polygon
|
49
|
+
when /geography\(GeometryCollection/i, /geometry\(GeometryCollection/i
|
50
|
+
:st_geometry_collection
|
51
|
+
when /geography/i
|
52
|
+
:st_geography
|
53
|
+
when /geometry/i
|
54
|
+
:st_geometry
|
55
|
+
end
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_spatial_options(column, spec)
|
62
|
+
if column.sql_type =~ /^(geometry|geography)\(([^,\)]+)(?:,(\d+))?\)/i
|
63
|
+
spatial_type = $1
|
64
|
+
geom_type = $2
|
65
|
+
srid = $3&.to_i
|
66
|
+
|
67
|
+
# Add SRID if not default
|
68
|
+
if srid && srid != default_srid(spatial_type)
|
69
|
+
spec[:srid] = srid
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check for dimension modifiers
|
73
|
+
if geom_type =~ /^(\w+?)(Z|M|ZM)$/i
|
74
|
+
dimensions = $2.upcase
|
75
|
+
spec[:has_z] = true if dimensions.include?("Z")
|
76
|
+
spec[:has_m] = true if dimensions.include?("M")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def default_srid(spatial_type)
|
82
|
+
spatial_type.downcase == "geography" ? 4326 : 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Prepend to PostgreSQL's SchemaDumper
|
88
|
+
module PostgreSQL
|
89
|
+
class SchemaDumper
|
90
|
+
prepend PostGIS::SchemaDumper
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module SchemaStatements
|
7
|
+
# Override to handle PostGIS column types
|
8
|
+
def columns(table_name)
|
9
|
+
column_definitions(table_name).map do |field|
|
10
|
+
new_column_from_field(table_name, field, @type_map)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override column type lookup to handle PostGIS types
|
15
|
+
def column_type_for(column_name)
|
16
|
+
column_name = column_name.to_s
|
17
|
+
|
18
|
+
# Check if it's a known PostGIS type
|
19
|
+
if column_name =~ /^(geometry|geography)/
|
20
|
+
# Extract the base type name from definitions like "geometry(Point,4326)"
|
21
|
+
base_type = case column_name
|
22
|
+
when /\(Point/i then :point
|
23
|
+
when /\(LineString/i then :line_string
|
24
|
+
when /\(Polygon/i then :polygon
|
25
|
+
when /\(MultiPoint/i then :multi_point
|
26
|
+
when /\(MultiLineString/i then :multi_line_string
|
27
|
+
when /\(MultiPolygon/i then :multi_polygon
|
28
|
+
when /\(GeometryCollection/i then :geometry_collection
|
29
|
+
when /^geography/i then :geography
|
30
|
+
when /^geometry/i then :geometry
|
31
|
+
else
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
return base_type if base_type
|
36
|
+
end
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Override to handle PostGIS types in column fetching
|
44
|
+
def column_definitions(table_name)
|
45
|
+
query = <<~SQL
|
46
|
+
SELECT#{' '}
|
47
|
+
a.attname AS column_name,
|
48
|
+
format_type(a.atttypid, a.atttypmod) AS sql_type,
|
49
|
+
pg_get_expr(d.adbin, d.adrelid) AS column_default,
|
50
|
+
a.attnotnull AS not_null,
|
51
|
+
a.atttypid AS type_id,
|
52
|
+
a.atttypmod AS type_mod,
|
53
|
+
c.collname AS collation,
|
54
|
+
col_description(pg_class.oid, a.attnum) AS comment,
|
55
|
+
#{supports_identity_columns? ? 'attidentity' : quote('')} AS identity,
|
56
|
+
#{supports_virtual_columns? ? 'attgenerated' : quote('')} AS attgenerated
|
57
|
+
FROM pg_attribute a
|
58
|
+
LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
59
|
+
LEFT JOIN pg_type t ON a.atttypid = t.oid
|
60
|
+
LEFT JOIN pg_collation c ON a.attcollation = c.oid
|
61
|
+
JOIN pg_class ON pg_class.oid = a.attrelid
|
62
|
+
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
|
63
|
+
AND a.attnum > 0
|
64
|
+
AND NOT a.attisdropped
|
65
|
+
ORDER BY a.attnum
|
66
|
+
SQL
|
67
|
+
|
68
|
+
execute_and_clear(query, "SCHEMA", allow_retry: true, uses_transaction: false) do |result|
|
69
|
+
result.map do |row|
|
70
|
+
{
|
71
|
+
"column_name" => row["column_name"],
|
72
|
+
"sql_type" => row["sql_type"],
|
73
|
+
"type_id" => row["type_id"],
|
74
|
+
"type_mod" => row["type_mod"],
|
75
|
+
"column_default" => row["column_default"],
|
76
|
+
"is_nullable" => row["not_null"] == "f" ? "YES" : "NO",
|
77
|
+
"collation" => row["collation"],
|
78
|
+
"comment" => row["comment"],
|
79
|
+
"identity" => row["identity"],
|
80
|
+
"attgenerated" => row["attgenerated"]
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create column from field data, handling PostGIS types
|
87
|
+
def new_column_from_field(table_name, field, type_map)
|
88
|
+
type_metadata = fetch_type_metadata(field["column_name"], field["sql_type"], field, type_map)
|
89
|
+
default_value = extract_value_from_default(field["column_default"])
|
90
|
+
default_function = extract_default_function(field["column_default"], default_value)
|
91
|
+
|
92
|
+
PostgreSQL::Column.new(
|
93
|
+
field["column_name"],
|
94
|
+
default_value,
|
95
|
+
type_metadata,
|
96
|
+
field["is_nullable"] == "YES",
|
97
|
+
default_function,
|
98
|
+
comment: field["comment"].presence,
|
99
|
+
identity: field["identity"].presence
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Fetch type metadata, handling PostGIS types
|
104
|
+
def fetch_type_metadata(column_name, sql_type, field, type_map)
|
105
|
+
cast_type = if sql_type =~ /^(geometry|geography)/i
|
106
|
+
# Handle PostGIS types
|
107
|
+
type_name = case sql_type
|
108
|
+
when /\(Point/i then :point
|
109
|
+
when /\(LineString/i then :line_string
|
110
|
+
when /\(Polygon/i then :polygon
|
111
|
+
when /\(MultiPoint/i then :multi_point
|
112
|
+
when /\(MultiLineString/i then :multi_line_string
|
113
|
+
when /\(MultiPolygon/i then :multi_polygon
|
114
|
+
when /\(GeometryCollection/i then :geometry_collection
|
115
|
+
when /^geography/i then :geography
|
116
|
+
when /^geometry/i then :geometry
|
117
|
+
end
|
118
|
+
|
119
|
+
lookup_cast_type(type_name.to_s)
|
120
|
+
else
|
121
|
+
type_map.fetch(field["type_id"].to_i, field["type_mod"].to_i) do
|
122
|
+
lookup_cast_type(sql_type)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
simple_type = SqlTypeMetadata.new(
|
127
|
+
sql_type: sql_type,
|
128
|
+
type: cast_type
|
129
|
+
)
|
130
|
+
|
131
|
+
PostgreSQL::TypeMetadata.new(simple_type)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module SpatialColumnMethods
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
define_column_methods(*GEOMETRIC_TYPES)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
# We reuse existing column definition options:
|
15
|
+
# - limit: SRID value
|
16
|
+
# - precision: has_z flag
|
17
|
+
# - scale: true for geography, false/nil for geometry
|
18
|
+
# + standard options like null, default, comment, etc.
|
19
|
+
def validate_column_definition(column_type, options)
|
20
|
+
super
|
21
|
+
|
22
|
+
return unless GEOMETRIC_TYPES.include?(column_type.to_sym)
|
23
|
+
|
24
|
+
if options[:limit] # SRID validation
|
25
|
+
srid = options[:limit]
|
26
|
+
if options[:scale] # geography type
|
27
|
+
unless srid == 4326
|
28
|
+
raise ArgumentError, "Geography columns only support SRID 4326. Got: #{srid}"
|
29
|
+
end
|
30
|
+
else # geometry type
|
31
|
+
unless srid.is_a?(Integer) && srid >= 0 && srid <= 999999
|
32
|
+
raise ArgumentError, "SRID must be between 0 and 999999. Got: #{srid}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module PostgreSQL
|
8
|
+
SchemaCreation.class_eval do
|
9
|
+
private
|
10
|
+
|
11
|
+
def visit_ColumnDefinition(o)
|
12
|
+
if o.type.to_s =~ /^st_/
|
13
|
+
# Parse options from limit if it's our custom format
|
14
|
+
srid = nil
|
15
|
+
has_z = false
|
16
|
+
has_m = false
|
17
|
+
geographic = false
|
18
|
+
|
19
|
+
# Check if we have spatial SQL string from table definition
|
20
|
+
if o.options[:spatial_sql] && o.options[:spatial_sql].include?(",")
|
21
|
+
geo_part, srid_part = o.options[:spatial_sql].split(",", 2)
|
22
|
+
srid = srid_part.to_i if srid_part && !srid_part.empty?
|
23
|
+
if geo_part
|
24
|
+
# Check for Z and M dimensions at the end of the geometry type
|
25
|
+
has_z = geo_part.end_with?("Z") || geo_part.end_with?("ZM")
|
26
|
+
has_m = geo_part.end_with?("M") || geo_part.end_with?("ZM")
|
27
|
+
end
|
28
|
+
elsif o.limit.is_a?(String) && o.limit.include?(",")
|
29
|
+
# Fallback to parsing limit string (for schema loading)
|
30
|
+
geo_part, srid_part = o.limit.split(",", 2)
|
31
|
+
srid = srid_part.to_i if srid_part && !srid_part.empty?
|
32
|
+
if geo_part
|
33
|
+
has_z = geo_part.end_with?("Z") || geo_part.end_with?("ZM")
|
34
|
+
has_m = geo_part.end_with?("M") || geo_part.end_with?("ZM")
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# Use individual column options if available
|
38
|
+
srid = o.options[:srid] if o.options.key?(:srid)
|
39
|
+
has_z = o.options[:has_z] if o.options.key?(:has_z)
|
40
|
+
has_m = o.options[:has_m] if o.options.key?(:has_m)
|
41
|
+
geographic = o.options[:geographic] if o.options.key?(:geographic)
|
42
|
+
end
|
43
|
+
|
44
|
+
sql_type = type_to_sql(o.type.to_sym,
|
45
|
+
limit: o.options[:spatial_sql] || o.limit,
|
46
|
+
precision: o.precision,
|
47
|
+
scale: o.scale,
|
48
|
+
srid: srid,
|
49
|
+
geographic: geographic,
|
50
|
+
has_z: has_z,
|
51
|
+
has_m: has_m,
|
52
|
+
geo_part: geo_part)
|
53
|
+
column_sql = "#{quote_column_name(o.name)} #{sql_type}"
|
54
|
+
add_column_options!(column_sql, column_options(o))
|
55
|
+
column_sql
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def type_to_sql(type, limit: nil, precision: nil, scale: nil, geographic: false, srid: nil, has_z: false, has_m: false, geo_part: nil, **options)
|
62
|
+
if type.to_s =~ /^st_/
|
63
|
+
# Use geo_part if available (from limit parsing), otherwise derive from type
|
64
|
+
if geo_part && geo_part != "GEOGRAPHY" && geo_part != "GEOMETRY"
|
65
|
+
# Convert from PostGIS SQL format (e.g., MULTIPOLYGON) to our internal format (e.g., multi_polygon)
|
66
|
+
# First strip any dimension suffixes (Z, M, ZM)
|
67
|
+
base_geo_part = geo_part.upcase.gsub(/[ZM]+$/, "")
|
68
|
+
geometric_type = case base_geo_part
|
69
|
+
when "MULTIPOINT" then "multi_point"
|
70
|
+
when "MULTILINESTRING" then "multi_line_string"
|
71
|
+
when "MULTIPOLYGON" then "multi_polygon"
|
72
|
+
when "GEOMETRYCOLLECTION" then "geometry_collection"
|
73
|
+
when "LINESTRING" then "line_string"
|
74
|
+
else
|
75
|
+
base_geo_part.downcase
|
76
|
+
end
|
77
|
+
else
|
78
|
+
geometric_type = type.to_s.sub(/^st_/, "")
|
79
|
+
end
|
80
|
+
# Only use geography if explicitly requested via type or option
|
81
|
+
is_geography = type.to_s == "st_geography" || geographic == true
|
82
|
+
result = PostGIS::SpatialColumnType.new(
|
83
|
+
geometric_type,
|
84
|
+
srid,
|
85
|
+
has_z: has_z,
|
86
|
+
has_m: has_m,
|
87
|
+
geography: is_geography
|
88
|
+
).to_sql
|
89
|
+
result
|
90
|
+
else
|
91
|
+
super(type, limit: limit, precision: precision, scale: scale, **options)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
module PostGIS
|
98
|
+
class SpatialColumnType
|
99
|
+
VALID_TYPES = %w[
|
100
|
+
point line_string polygon multi_point
|
101
|
+
multi_line_string multi_polygon geometry_collection geography geometry
|
102
|
+
].freeze
|
103
|
+
|
104
|
+
attr_reader :type, :srid, :has_z, :has_m, :geography
|
105
|
+
|
106
|
+
def initialize(type, srid = nil, has_z: false, has_m: false, geography: false)
|
107
|
+
@type = type.to_s.downcase
|
108
|
+
@srid = srid
|
109
|
+
@has_z = has_z
|
110
|
+
@has_m = has_m
|
111
|
+
@geography = geography || @type == "geography"
|
112
|
+
|
113
|
+
validate_type!
|
114
|
+
validate_srid!
|
115
|
+
validate_dimensions!
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_sql
|
119
|
+
base_type = @geography ? "geography" : "geometry"
|
120
|
+
return base_type if @type == "geography"
|
121
|
+
|
122
|
+
type_with_dimensions = build_type_with_dimensions
|
123
|
+
# Include SRID if specified and not the default for the type
|
124
|
+
# Geography defaults to 4326, geometry defaults to 0
|
125
|
+
should_include_srid = @srid &&
|
126
|
+
((@geography && @srid != 4326) || (!@geography && @srid != 0))
|
127
|
+
|
128
|
+
if should_include_srid
|
129
|
+
"#{base_type}(#{type_with_dimensions},#{@srid})"
|
130
|
+
else
|
131
|
+
"#{base_type}(#{type_with_dimensions})"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def validate_type!
|
138
|
+
unless VALID_TYPES.include?(@type)
|
139
|
+
raise ArgumentError, "Invalid geometry type: #{@type}. Valid types are: #{VALID_TYPES.join(', ')}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def validate_srid!
|
144
|
+
return unless @srid
|
145
|
+
|
146
|
+
if @geography && @srid != 4326
|
147
|
+
raise ArgumentError, "Invalid SRID for geography type: #{@srid}. The SRID must be 4326 or nil."
|
148
|
+
end
|
149
|
+
|
150
|
+
unless @srid.is_a?(Integer) && @srid >= 0 && @srid <= 999_999
|
151
|
+
raise ArgumentError, "Invalid SRID #{@srid}. The SRID must be within the range 0-999999."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def validate_dimensions!
|
156
|
+
# All geometry types can have Z, M, or ZM dimensions
|
157
|
+
end
|
158
|
+
|
159
|
+
def build_type_with_dimensions
|
160
|
+
type_name = @type.camelize
|
161
|
+
if @has_z && @has_m
|
162
|
+
type_name += "ZM"
|
163
|
+
elsif @has_z
|
164
|
+
type_name += "Z"
|
165
|
+
elsif @has_m
|
166
|
+
type_name += "M"
|
167
|
+
end
|
168
|
+
type_name
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module TableDefinition
|
7
|
+
# Define spatial column methods
|
8
|
+
%i[st_point st_line_string st_polygon st_multi_point
|
9
|
+
st_multi_line_string st_multi_polygon st_geometry_collection
|
10
|
+
st_geometry st_geography].each do |spatial_type|
|
11
|
+
define_method(spatial_type) do |name, **options|
|
12
|
+
column(name, spatial_type, **options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Override new_column_definition to handle spatial column options
|
17
|
+
def new_column_definition(name, type, **options)
|
18
|
+
col_type = if type.to_sym == :virtual
|
19
|
+
options[:type]
|
20
|
+
else
|
21
|
+
type
|
22
|
+
end
|
23
|
+
|
24
|
+
if spatial_column_type?(col_type)
|
25
|
+
if (limit = options.delete(:limit)) && limit.is_a?(::Hash)
|
26
|
+
options.merge!(limit)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Set geographic option for geography types
|
30
|
+
if col_type.to_sym == :st_geography || col_type.to_sym == :geography
|
31
|
+
options[:geographic] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
geo_type = ColumnDefinitionUtils.geo_type(options[:type] || type)
|
35
|
+
base_type = determine_base_type(col_type, options)
|
36
|
+
|
37
|
+
|
38
|
+
# Create hash format limit for column metadata
|
39
|
+
spatial_limit = {}
|
40
|
+
spatial_limit[:type] = col_type.to_s
|
41
|
+
spatial_limit[:srid] = options[:srid] if options[:srid] && options[:srid] != (options[:geographic] ? 4326 : 0)
|
42
|
+
spatial_limit[:has_z] = options[:has_z] if options[:has_z]
|
43
|
+
spatial_limit[:has_m] = options[:has_m] if options[:has_m]
|
44
|
+
spatial_limit[:geographic] = options[:geographic] if options[:geographic]
|
45
|
+
|
46
|
+
# Use hash format as limit for spatial columns, string format for SQL generation
|
47
|
+
options[:limit] = spatial_limit.empty? ? nil : spatial_limit
|
48
|
+
options[:spatial_type] = geo_type
|
49
|
+
options[:spatial_sql] = ColumnDefinitionUtils.limit_from_options(geo_type, options)
|
50
|
+
|
51
|
+
|
52
|
+
column = super(name, base_type, **options)
|
53
|
+
else
|
54
|
+
column = super(name, type, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
column
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Allow spatial-specific options in column definitions
|
63
|
+
def valid_column_definition_options
|
64
|
+
super + [ :srid, :has_z, :has_m, :geographic, :spatial_type, :spatial_sql ]
|
65
|
+
end
|
66
|
+
|
67
|
+
def spatial_column_type?(type)
|
68
|
+
type.to_s.start_with?("st_") ||
|
69
|
+
[ :geography, :geometry, :geometry_collection, :line_string,
|
70
|
+
:multi_line_string, :multi_point, :multi_polygon, :polygon ].include?(type.to_sym)
|
71
|
+
end
|
72
|
+
|
73
|
+
def determine_base_type(col_type, options)
|
74
|
+
case col_type.to_sym
|
75
|
+
when :st_geography, :geography
|
76
|
+
:st_geography
|
77
|
+
else
|
78
|
+
# Only use geography if explicitly requested
|
79
|
+
if options[:geographic] == true
|
80
|
+
:st_geography
|
81
|
+
else
|
82
|
+
# Convert legacy types to st_ prefixed equivalents
|
83
|
+
convert_to_st_type(col_type)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def convert_to_st_type(col_type)
|
89
|
+
case col_type.to_sym
|
90
|
+
when :geometry then :st_geometry
|
91
|
+
when :geometry_collection then :st_geometry_collection
|
92
|
+
when :line_string then :st_line_string
|
93
|
+
when :multi_line_string then :st_multi_line_string
|
94
|
+
when :multi_point then :st_multi_point
|
95
|
+
when :multi_polygon then :st_multi_polygon
|
96
|
+
when :polygon then :st_polygon
|
97
|
+
else
|
98
|
+
# Already an st_ type or unknown type
|
99
|
+
col_type.to_sym
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Custom table definition class that includes spatial support
|
105
|
+
class SpatialTableDefinition < ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
|
106
|
+
include TableDefinition
|
107
|
+
end
|
108
|
+
|
109
|
+
module ColumnDefinitionUtils
|
110
|
+
class << self
|
111
|
+
def geo_type(type = "GEOMETRY")
|
112
|
+
g_type = type.to_s.delete("_").upcase
|
113
|
+
case g_type
|
114
|
+
when "STPOINT" then "POINT"
|
115
|
+
when "STPOLYGON" then "POLYGON"
|
116
|
+
when "STLINESTRING" then "LINESTRING"
|
117
|
+
when "STMULTIPOINT" then "MULTIPOINT"
|
118
|
+
when "STMULTILINESTRING" then "MULTILINESTRING"
|
119
|
+
when "STMULTIPOLYGON" then "MULTIPOLYGON"
|
120
|
+
when "STGEOMETRYCOLLECTION" then "GEOMETRYCOLLECTION"
|
121
|
+
when "STGEOMETRY" then "GEOMETRY"
|
122
|
+
when "STGEOGRAPHY" then "GEOGRAPHY"
|
123
|
+
else
|
124
|
+
"GEOMETRY" # Default fallback
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def limit_from_options(type, options = {})
|
129
|
+
has_z = options[:has_z] ? "Z" : ""
|
130
|
+
has_m = options[:has_m] ? "M" : ""
|
131
|
+
srid = options[:srid] || default_srid(options)
|
132
|
+
field_type = [ type, has_z, has_m ].compact.join
|
133
|
+
"#{field_type},#{srid}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def default_srid(options)
|
137
|
+
# Geography columns default to SRID 4326, geometry columns to 0
|
138
|
+
options[:geographic] ? 4326 : 0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spatial"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module PostGIS
|
8
|
+
module Type
|
9
|
+
class Geography < Spatial
|
10
|
+
def initialize(srid: 4326, has_z: false, has_m: false)
|
11
|
+
super(geo_type: "geography", srid: srid, has_z: has_z, has_m: has_m, geographic: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
:st_geography
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spatial"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module PostGIS
|
8
|
+
module Type
|
9
|
+
class Geometry < Spatial
|
10
|
+
def initialize(srid: 0, has_z: false, has_m: false)
|
11
|
+
super(geo_type: "geometry", srid: srid, has_z: has_z, has_m: has_m)
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
:st_geometry
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spatial"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module PostGIS
|
8
|
+
module Type
|
9
|
+
class GeometryCollection < Spatial
|
10
|
+
def initialize(srid: 0, has_z: false, has_m: false)
|
11
|
+
super(geo_type: "geometry_collection", srid: srid, has_z: has_z, has_m: has_m)
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
:st_geometry_collection
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|