activerecord-trilogis-adapter 7.0.2 → 8.0.1

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.
@@ -1,62 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord # :nodoc:
4
- module ConnectionAdapters # :nodoc:
5
- module Trilogis # :nodoc:
6
- class SpatialColumn < ConnectionAdapters::MySQL::Column # :nodoc:
7
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, spatial: nil, **)
8
- @sql_type_metadata = sql_type_metadata
9
- if spatial
10
- # This case comes from an entry in the geometry_columns table
11
- set_geometric_type_from_name(spatial[:type])
12
- @srid = spatial[:srid].to_i
13
- elsif sql_type =~ /geometry|point|linestring|polygon/i
14
- build_from_sql_type(sql_type_metadata.sql_type)
15
- elsif sql_type_metadata.sql_type =~ /geometry|point|linestring|polygon/i
16
- # A geometry column with no geometry_columns entry.
17
- # @geometric_type = geo_type_from_sql_type(sql_type)
18
- build_from_sql_type(sql_type_metadata.sql_type)
19
- end
20
- super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
21
- if spatial? && @srid
22
- @limit = {type: geometric_type.type_name.underscore, srid: @srid}
23
- end
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # Add spatial? method to MySQL::Column for compatibility
6
+ module MySQL
7
+ class Column
8
+ def spatial?
9
+ false
24
10
  end
11
+ end
12
+ end
25
13
 
26
- attr_reader :geometric_type, :srid
14
+ module Trilogis
15
+ class SpatialColumn < ActiveRecord::ConnectionAdapters::MySQL::Column
16
+ attr_reader :geometric_type, :srid, :geo_type_name
27
17
 
28
- def has_z
29
- false
30
- end
18
+ # Map SQL type strings to RGeo type classes
19
+ GEOMETRIC_TYPES = {
20
+ "geometry" => RGeo::Feature::Geometry,
21
+ "point" => RGeo::Feature::Point,
22
+ "linestring" => RGeo::Feature::LineString,
23
+ "polygon" => RGeo::Feature::Polygon,
24
+ "multipoint" => RGeo::Feature::MultiPoint,
25
+ "multilinestring" => RGeo::Feature::MultiLineString,
26
+ "multipolygon" => RGeo::Feature::MultiPolygon,
27
+ "geometrycollection" => RGeo::Feature::GeometryCollection
28
+ }.freeze
31
29
 
32
- def has_m
33
- false
34
- end
30
+ def initialize(name, default, sql_type_metadata = nil, null = true,
31
+ default_function = nil, collation: nil, comment: nil, spatial_info: nil, **)
32
+ super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
35
33
 
36
- alias has_z? has_z
37
- alias has_m? has_m
34
+ return unless spatial?
38
35
 
39
- def multi?
40
- /^(geometrycollection|multi)/i.match?(sql_type)
36
+ if spatial_info
37
+ # Use spatial info from INFORMATION_SCHEMA if available
38
+ @geo_type_name = spatial_info[:type].to_s.downcase
39
+ @geometric_type = GEOMETRIC_TYPES[@geo_type_name] || RGeo::Feature::Geometry
40
+ @srid = spatial_info[:srid] || 0
41
+ @has_z = spatial_info[:has_z] || false
42
+ @has_m = spatial_info[:has_m] || false
43
+ else
44
+ # Fallback to extracting from SQL type
45
+ @geo_type_name = sql_type_metadata.sql_type.to_s.downcase
46
+ type_info = Type::Spatial.new(@geo_type_name)
47
+ @geometric_type = GEOMETRIC_TYPES[@geo_type_name] || RGeo::Feature::Geometry
48
+ @srid = type_info.srid || 0
49
+ @has_z = false
50
+ @has_m = false
51
+ end
41
52
  end
42
53
 
43
- def limit
44
- spatial? ? @limit : super
54
+ def spatial?
55
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type_metadata.sql_type.downcase)
45
56
  end
46
57
 
47
- def spatial?
48
- %i[geometry].include?(@sql_type_metadata.type)
58
+ def has_z?
59
+ @has_z || false
60
+ end
61
+
62
+ def has_m?
63
+ @has_m || false
49
64
  end
50
65
 
51
- private
66
+ # Return Rails type for schema dumper
67
+ # Returns the actual geometric type (point, linestring, etc.) as symbol
68
+ # This allows schema dumper to generate t.point, t.linestring, etc.
69
+ def type
70
+ return super unless spatial?
52
71
 
53
- def set_geometric_type_from_name(name)
54
- @geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
72
+ # Return actual geometric type as symbol
73
+ # This matches PostGIS approach and enables proper schema dumping
74
+ @geo_type_name&.to_sym || :geometry
55
75
  end
56
76
 
57
- def build_from_sql_type(sql_type)
58
- geo_type, @srid = Type::Spatial.parse_sql_type(sql_type)
59
- set_geometric_type_from_name(geo_type)
77
+ # Return limit as hash with spatial metadata for schema dumper
78
+ # Only includes SRID (type is already in column type)
79
+ def limit
80
+ return super unless spatial?
81
+
82
+ # Only include SRID in limit
83
+ # Type information is in the type() method
84
+ { srid: @srid }.compact
60
85
  end
61
86
  end
62
87
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord # :nodoc:
4
- module ConnectionAdapters # :nodoc:
5
- module Trilogis # :nodoc:
6
- # Do spatial sql queries for column info and memoize that info.
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trilogis
6
+ # Queries MySQL INFORMATION_SCHEMA for spatial column metadata
7
+ # and caches the results for performance
7
8
  class SpatialColumnInfo
8
9
  def initialize(adapter, table_name)
9
10
  @adapter = adapter
@@ -11,30 +12,48 @@ module ActiveRecord # :nodoc:
11
12
  end
12
13
 
13
14
  def all
14
- info = @adapter.query(
15
- "SELECT column_name, srs_id, column_type FROM INFORMATION_SCHEMA.Columns WHERE table_name='#{@table_name}'"
16
- )
15
+ # Query MySQL's ST_GEOMETRY_COLUMNS view for SRID info
16
+ # This view properly returns SRS_ID for spatial columns
17
+ sql = <<~SQL.squish
18
+ SELECT
19
+ gc.COLUMN_NAME as column_name,
20
+ gc.SRS_ID as srs_id,
21
+ gc.GEOMETRY_TYPE_NAME as geometry_type,
22
+ c.COLUMN_TYPE as column_type
23
+ FROM INFORMATION_SCHEMA.ST_GEOMETRY_COLUMNS gc
24
+ JOIN INFORMATION_SCHEMA.COLUMNS c
25
+ ON gc.TABLE_SCHEMA = c.TABLE_SCHEMA
26
+ AND gc.TABLE_NAME = c.TABLE_NAME
27
+ AND gc.COLUMN_NAME = c.COLUMN_NAME
28
+ WHERE gc.TABLE_SCHEMA = DATABASE()
29
+ AND gc.TABLE_NAME = #{@adapter.quote(@table_name)}
30
+ SQL
17
31
 
18
32
  result = {}
19
- info.each do |row|
20
- name = row[0]
21
- type = row[2]
22
- type.sub!(/m$/, "")
23
- result[name] = {
24
- name: name,
25
- srid: row[1].to_i,
26
- type: type,
33
+ @adapter.exec_query(sql, "SCHEMA").each do |row|
34
+ column_name = row["column_name"] || row["COLUMN_NAME"]
35
+ srs_id = row["srs_id"] || row["SRS_ID"]
36
+ row["geometry_type"] || row["GEOMETRY_TYPE"]
37
+ column_type = (row["column_type"] || row["COLUMN_TYPE"]).to_s.sub(/m$/, "")
38
+
39
+ result[column_name] = {
40
+ name: column_name,
41
+ srid: srs_id.to_i,
42
+ type: column_type
27
43
  }
28
44
  end
29
45
  result
30
46
  end
31
47
 
32
- # do not query the database for non-spatial columns/tables
33
- def get(column_name, type)
34
- return unless TrilogisAdapter.spatial_column_options(type.to_sym)
48
+ # Get spatial info for a specific column if it's spatial
49
+ # Returns nil for non-spatial columns to avoid unnecessary queries
50
+ def get(column_name, sql_type)
51
+ # Only query for known spatial types
52
+ return unless TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type.to_s.downcase)
35
53
 
36
- @spatial_column_info ||= all
37
- @spatial_column_info[column_name]
54
+ # Don't memoize - always query fresh data to avoid stale cache issues
55
+ # when columns are added during tests
56
+ all[column_name]
38
57
  end
39
58
  end
40
59
  end
@@ -3,18 +3,92 @@
3
3
  module RGeo
4
4
  module ActiveRecord
5
5
  module Trilogis
6
+ # MySQL-specific spatial expressions
6
7
  module SpatialExpressions
8
+ # MySQL ST_Distance_Sphere for geographic distance calculations
7
9
  def st_distance_sphere(rhs, units = nil)
8
10
  args = [self, rhs]
9
11
  args << units.to_s if units
10
12
  SpatialNamedFunction.new("ST_Distance_Sphere", args, [false, true, true, false])
11
13
  end
14
+
15
+ # Additional MySQL spatial functions
16
+ def st_buffer(distance)
17
+ SpatialNamedFunction.new("ST_Buffer", [self, distance], [false, true, false])
18
+ end
19
+
20
+ def st_contains(rhs)
21
+ SpatialNamedFunction.new("ST_Contains", [self, rhs], [false, true, true])
22
+ end
23
+
24
+ def st_within(rhs)
25
+ SpatialNamedFunction.new("ST_Within", [self, rhs], [false, true, true])
26
+ end
27
+
28
+ def st_intersects(rhs)
29
+ SpatialNamedFunction.new("ST_Intersects", [self, rhs], [false, true, true])
30
+ end
31
+
32
+ def st_crosses(rhs)
33
+ SpatialNamedFunction.new("ST_Crosses", [self, rhs], [false, true, true])
34
+ end
35
+
36
+ def st_touches(rhs)
37
+ SpatialNamedFunction.new("ST_Touches", [self, rhs], [false, true, true])
38
+ end
39
+
40
+ def st_overlaps(rhs)
41
+ SpatialNamedFunction.new("ST_Overlaps", [self, rhs], [false, true, true])
42
+ end
43
+
44
+ def st_equals(rhs)
45
+ SpatialNamedFunction.new("ST_Equals", [self, rhs], [false, true, true])
46
+ end
47
+
48
+ def st_disjoint(rhs)
49
+ SpatialNamedFunction.new("ST_Disjoint", [self, rhs], [false, true, true])
50
+ end
51
+
52
+ def st_area
53
+ SpatialNamedFunction.new("ST_Area", [self], [false, true])
54
+ end
55
+
56
+ def st_length
57
+ SpatialNamedFunction.new("ST_Length", [self], [false, true])
58
+ end
59
+
60
+ def st_centroid
61
+ SpatialNamedFunction.new("ST_Centroid", [self], [false, true])
62
+ end
63
+
64
+ def st_envelope
65
+ SpatialNamedFunction.new("ST_Envelope", [self], [false, true])
66
+ end
67
+
68
+ def st_astext
69
+ SpatialNamedFunction.new("ST_AsText", [self], [false, true])
70
+ end
71
+
72
+ def st_asbinary
73
+ SpatialNamedFunction.new("ST_AsBinary", [self], [false, true])
74
+ end
75
+
76
+ def st_srid
77
+ SpatialNamedFunction.new("ST_SRID", [self], [false, true])
78
+ end
12
79
  end
13
80
  end
14
81
  end
15
82
  end
16
83
 
17
- # Allow chaining of spatial expressions from attributes
18
- Arel::Attribute.include RGeo::ActiveRecord::Trilogis::SpatialExpressions
19
- RGeo::ActiveRecord::SpatialConstantNode.include RGeo::ActiveRecord::Trilogis::SpatialExpressions
20
- RGeo::ActiveRecord::SpatialNamedFunction.include RGeo::ActiveRecord::Trilogis::SpatialExpressions
84
+ # Allow chaining of spatial expressions from Arel attributes
85
+ Arel::Attribute.include RGeo::ActiveRecord::Trilogis::SpatialExpressions if defined?(Arel::Attribute)
86
+
87
+ # Include in RGeo spatial nodes if they exist
88
+ if defined?(RGeo::ActiveRecord::SpatialConstantNode)
89
+ RGeo::ActiveRecord::SpatialConstantNode.include RGeo::ActiveRecord::Trilogis::SpatialExpressions
90
+ end
91
+
92
+ if defined?(RGeo::ActiveRecord::SpatialNamedFunction)
93
+ RGeo::ActiveRecord::SpatialNamedFunction.include RGeo::ActiveRecord::Trilogis::SpatialExpressions
94
+ end
@@ -1,36 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord # :nodoc:
4
- module ConnectionAdapters # :nodoc:
5
- module Trilogis # :nodoc:
6
- class TableDefinition < MySQL::TableDefinition # :nodoc:
7
- include ColumnMethods
8
- # super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
9
- def new_column_definition(name, type, **options)
10
- if (info = TrilogisAdapter.spatial_column_options(type.to_sym))
11
- if (limit = options.delete(:limit)) && limit.is_a?(::Hash)
12
- options.merge!(limit)
13
- end
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trilogis
6
+ module ColumnMethods
7
+ # Generic spatial column method
8
+ def spatial(name, options = {})
9
+ raise "You must set a type. For example: 't.spatial :location, type: :point'" unless options[:type]
14
10
 
15
- geo_type = ColumnDefinitionUtils.geo_type(options[:type] || type || info[:type])
11
+ column(name, options[:type], **options)
12
+ end
16
13
 
17
- options[:spatial_type] = geo_type
18
- column = super(name, geo_type.downcase.to_sym, **options)
19
- else
20
- column = super(name, type, **options)
21
- end
14
+ # Define spatial column types with both underscore and non-underscore versions
15
+ def geometry(name, options = {})
16
+ column(name, :geometry, **options)
17
+ end
18
+
19
+ def geometrycollection(name, options = {})
20
+ column(name, :geometrycollection, **options)
21
+ end
22
+ alias geometry_collection geometrycollection
23
+
24
+ def linestring(name, options = {})
25
+ column(name, :linestring, **options)
26
+ end
27
+ alias line_string linestring
28
+
29
+ def multilinestring(name, options = {})
30
+ column(name, :multilinestring, **options)
31
+ end
32
+ alias multi_line_string multilinestring
33
+
34
+ def multipoint(name, options = {})
35
+ column(name, :multipoint, **options)
36
+ end
37
+ alias multi_point multipoint
38
+
39
+ def multipolygon(name, options = {})
40
+ column(name, :multipolygon, **options)
41
+ end
42
+ alias multi_polygon multipolygon
43
+
44
+ def point(name, options = {})
45
+ column(name, :point, **options)
46
+ end
22
47
 
23
- column
48
+ def polygon(name, options = {})
49
+ column(name, :polygon, **options)
24
50
  end
25
51
  end
26
52
 
27
- module ColumnDefinitionUtils
28
- class << self
29
- def geo_type(type = "GEOMETRY")
30
- type.to_s.delete("_").upcase
53
+ class TableDefinition < ActiveRecord::ConnectionAdapters::MySQL::TableDefinition
54
+ include ColumnMethods
55
+
56
+ # Override column to handle spatial types
57
+ def column(name, type, index: nil, **options)
58
+ # Store spatial-specific options before processing
59
+ spatial_options = {
60
+ srid: options.delete(:srid),
61
+ has_m: options.delete(:has_m),
62
+ has_z: options.delete(:has_z),
63
+ geographic: options.delete(:geographic)
64
+ }.compact
65
+
66
+ # Call super to create column definition
67
+ result = super
68
+
69
+ # Add spatial options back to the column definition if it's a spatial type
70
+ if spatial_type?(type) && (col = @columns_hash[name.to_s])
71
+ spatial_options.each do |key, value|
72
+ col.options[key] = value
73
+ end
31
74
  end
75
+
76
+ # Add spatial index if requested
77
+ @indexes << [name, { type: :spatial }] if index && spatial_type?(type) && [true, :spatial].include?(index)
78
+
79
+ result
80
+ end
81
+
82
+ private
83
+
84
+ def spatial_type?(type)
85
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(type.to_s)
32
86
  end
33
87
  end
34
88
  end
89
+
90
+ # Include column methods in MySQL::Table for migrations
91
+ MySQL::Table.include Trilogis::ColumnMethods if defined?(MySQL::Table)
35
92
  end
36
93
  end
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogis
6
- VERSION = "7.0.2"
6
+ VERSION = "8.0.0"
7
7
  end
8
8
  end
9
9
  end