beh_spatial_adapter 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +201 -0
- data/VERSION +1 -0
- data/lib/spatial_adapter.rb +41 -0
- data/lib/spatial_adapter/common/raw_geom_info.rb +23 -0
- data/lib/spatial_adapter/common/schema_definitions.rb +10 -0
- data/lib/spatial_adapter/common/schema_dumper.rb +144 -0
- data/lib/spatial_adapter/common/spatial_column.rb +70 -0
- data/lib/spatial_adapter/common/table_definition.rb +14 -0
- data/lib/spatial_adapter/mysql.rb +102 -0
- data/lib/spatial_adapter/oracle_enhanced.rb +651 -0
- data/lib/spatial_adapter/postgresql.rb +395 -0
- data/lib/spatial_adapter/railtie.rb +13 -0
- data/rails/init.rb +16 -0
- data/spec/README.txt +16 -0
- data/spec/db/mysql_raw.rb +70 -0
- data/spec/db/postgis_raw.rb +190 -0
- data/spec/models/common.rb +65 -0
- data/spec/mysql/connection_adapter_spec.rb +106 -0
- data/spec/mysql/migration_spec.rb +64 -0
- data/spec/mysql/models_spec.rb +66 -0
- data/spec/mysql/schema_dumper_spec.rb +56 -0
- data/spec/postgresql/connection_adapter_spec.rb +227 -0
- data/spec/postgresql/migration_spec.rb +365 -0
- data/spec/postgresql/models_spec.rb +222 -0
- data/spec/postgresql/schema_dumper_spec.rb +79 -0
- data/spec/spec_helper.rb +97 -0
- metadata +120 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
module SpatialAdapter
|
2
|
+
module SpatialColumn
|
3
|
+
attr_reader :geometry_type, :srid, :with_z, :with_m
|
4
|
+
|
5
|
+
def initialize(name, default, sql_type = nil, null = true, srid=-1, with_z=false, with_m=false)
|
6
|
+
super(name, default, sql_type, null)
|
7
|
+
@geometry_type = geometry_simplified_type(@sql_type)
|
8
|
+
@srid = srid
|
9
|
+
@with_z = with_z
|
10
|
+
@with_m = with_m
|
11
|
+
end
|
12
|
+
|
13
|
+
def spatial?
|
14
|
+
!@geometry_type.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def geographic?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Redefines type_cast to add support for geometries
|
22
|
+
# alias_method :type_cast_without_spatial, :type_cast
|
23
|
+
def type_cast(value)
|
24
|
+
return nil if value.nil?
|
25
|
+
spatial? ? self.class.string_to_geometry(value) : super
|
26
|
+
end
|
27
|
+
|
28
|
+
#Redefines type_cast_code to add support for geometries.
|
29
|
+
#
|
30
|
+
#WARNING : Since ActiveRecord keeps only the string values directly returned from the database, it translates from these to the correct types everytime an attribute is read (using the code returned by this method), which is probably ok for simple types, but might be less than efficient for geometries. Also you cannot modify the geometry object returned directly or your change will not be saved.
|
31
|
+
# alias_method :type_cast_code_without_spatial, :type_cast_code
|
32
|
+
def type_cast_code(var_name)
|
33
|
+
spatial? ? "#{self.class.name}.string_to_geometry(#{var_name})" : super
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
#Redefines klass to add support for geometries
|
38
|
+
# alias_method :klass_without_spatial, :klass
|
39
|
+
def klass
|
40
|
+
spatial? ? GeoRuby::SimpleFeatures::Geometry : super
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Maps additional data types to base Rails/Arel types
|
46
|
+
#
|
47
|
+
# For Rails 3, only the types defined by Arel can be used. We'll
|
48
|
+
# use :string since the database returns the columns as hex strings.
|
49
|
+
def simplified_type(field_type)
|
50
|
+
case field_type
|
51
|
+
when /geography|geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :string
|
52
|
+
else super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# less simlpified geometric type to be use in migrations
|
57
|
+
def geometry_simplified_type(sql_type)
|
58
|
+
case sql_type
|
59
|
+
when /^point$/i then :point
|
60
|
+
when /^linestring$/i then :line_string
|
61
|
+
when /^polygon$/i then :polygon
|
62
|
+
when /^geometry$/i then :geometry
|
63
|
+
when /multipoint/i then :multi_point
|
64
|
+
when /multilinestring/i then :multi_line_string
|
65
|
+
when /multipolygon/i then :multi_polygon
|
66
|
+
when /geometrycollection/i then :geometry_collection
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
include SpatialAdapter
|
2
|
+
|
3
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.class_eval do
|
4
|
+
SpatialAdapter.geometry_data_types.keys.each do |column_type|
|
5
|
+
class_eval <<-EOV
|
6
|
+
def #{column_type}(*args)
|
7
|
+
options = args.extract_options!
|
8
|
+
column_names = args
|
9
|
+
|
10
|
+
column_names.each { |name| column(name, '#{column_type}', options) }
|
11
|
+
end
|
12
|
+
EOV
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spatial_adapter'
|
2
|
+
require 'active_record/connection_adapters/mysql_adapter'
|
3
|
+
|
4
|
+
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
|
5
|
+
include SpatialAdapter
|
6
|
+
|
7
|
+
def supports_geographic?
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_srid
|
12
|
+
4326
|
13
|
+
end
|
14
|
+
|
15
|
+
alias :original_native_database_types :native_database_types
|
16
|
+
def native_database_types
|
17
|
+
original_native_database_types.merge!(geometry_data_types)
|
18
|
+
end
|
19
|
+
|
20
|
+
alias :original_quote :quote
|
21
|
+
#Redefines the quote method to add behaviour for when a Geometry is encountered ; used when binding variables in find_by methods
|
22
|
+
def quote(value, column = nil)
|
23
|
+
if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
|
24
|
+
"GeomFromWKB(0x#{value.as_hex_wkb},#{value.srid})"
|
25
|
+
else
|
26
|
+
original_quote(value,column)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#Redefinition of columns to add the information that a column is geometric
|
31
|
+
def columns(table_name, name = nil)#:nodoc:
|
32
|
+
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
|
33
|
+
columns = []
|
34
|
+
result = execute(sql, name)
|
35
|
+
result.each do |field|
|
36
|
+
klass = field[1] =~ /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i ? ActiveRecord::ConnectionAdapters::SpatialMysqlColumn : ActiveRecord::ConnectionAdapters::MysqlColumn
|
37
|
+
columns << klass.new(field[0], field[4], field[1], field[2] == "YES")
|
38
|
+
end
|
39
|
+
result.free
|
40
|
+
columns
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
#operations relative to migrations
|
45
|
+
|
46
|
+
#Redefines add_index to support the case where the index is spatial
|
47
|
+
#If the :spatial key in the options table is true, then the sql string for a spatial index is created
|
48
|
+
def add_index(table_name,column_name,options = {})
|
49
|
+
index_name = options[:name] || index_name(table_name,:column => Array(column_name))
|
50
|
+
|
51
|
+
if options[:spatial]
|
52
|
+
execute "CREATE SPATIAL INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
#Check the nature of the index : If it is SPATIAL, it is indicated in the IndexDefinition object (redefined to add the spatial flag in spatial_adapter_common.rb)
|
59
|
+
def indexes(table_name, name = nil)#:nodoc:
|
60
|
+
indexes = []
|
61
|
+
current_index = nil
|
62
|
+
execute("SHOW KEYS FROM #{table_name}", name).each do |row|
|
63
|
+
if current_index != row[2]
|
64
|
+
next if row[2] == "PRIMARY" # skip the primary key
|
65
|
+
current_index = row[2]
|
66
|
+
indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.new(row[0], row[2], row[1] == "0", [], row[10] == "SPATIAL")
|
67
|
+
end
|
68
|
+
indexes.last.columns << row[4]
|
69
|
+
end
|
70
|
+
indexes
|
71
|
+
end
|
72
|
+
|
73
|
+
#Get the table creation options : Only the engine for now. The text encoding could also be parsed and returned here.
|
74
|
+
def options_for(table)
|
75
|
+
result = execute("show table status like '#{table}'")
|
76
|
+
engine = result.fetch_row[1]
|
77
|
+
if engine !~ /inno/i #inno is default so do nothing for it in order not to clutter the migration
|
78
|
+
"ENGINE=#{engine}"
|
79
|
+
else
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
module ActiveRecord
|
87
|
+
module ConnectionAdapters
|
88
|
+
class SpatialMysqlColumn < MysqlColumn
|
89
|
+
include SpatialAdapter::SpatialColumn
|
90
|
+
|
91
|
+
#MySql-specific geometry string parsing. By default, MySql returns geometries in strict wkb format with "0" characters in the first 4 positions.
|
92
|
+
def self.string_to_geometry(string)
|
93
|
+
return string unless string.is_a?(String)
|
94
|
+
begin
|
95
|
+
GeoRuby::SimpleFeatures::Geometry.from_ewkb(string[4..-1])
|
96
|
+
rescue Exception => exception
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,651 @@
|
|
1
|
+
require 'spatial_adapter'
|
2
|
+
require 'active_record/connection_adapters/oracle_enhanced_adapter'
|
3
|
+
|
4
|
+
include GeoRuby::SimpleFeatures
|
5
|
+
include SpatialAdapter
|
6
|
+
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
module ConnectionAdapters
|
10
|
+
class OracleEnhancedAdapter
|
11
|
+
|
12
|
+
def spatial?
|
13
|
+
rval = select_value("select object_id from all_objects where owner = 'MDSYS' and object_name = 'SDO_GEOMETRY'").to_i
|
14
|
+
rval > 0 ? true : false
|
15
|
+
end
|
16
|
+
|
17
|
+
def columns_without_cache(table_name, name = nil) #:nodoc:
|
18
|
+
table_name = table_name.to_s
|
19
|
+
# get ignored_columns by original table name
|
20
|
+
ignored_columns = ignored_table_columns(table_name)
|
21
|
+
|
22
|
+
(owner, desc_table_name, db_link) = @connection.describe(table_name)
|
23
|
+
|
24
|
+
@@do_not_prefetch_primary_key[table_name] =
|
25
|
+
!has_primary_key?(table_name, owner, desc_table_name, db_link) ||
|
26
|
+
has_primary_key_trigger?(table_name, owner, desc_table_name, db_link)
|
27
|
+
|
28
|
+
table_cols = <<-SQL
|
29
|
+
select column_name as name, data_type as sql_type, data_default, nullable,
|
30
|
+
decode(data_type, 'NUMBER', data_precision,
|
31
|
+
'FLOAT', data_precision,
|
32
|
+
'VARCHAR2', decode(char_used, 'C', char_length, data_length),
|
33
|
+
'CHAR', decode(char_used, 'C', char_length, data_length),
|
34
|
+
null) as limit,
|
35
|
+
decode(data_type, 'NUMBER', data_scale, null) as scale
|
36
|
+
from all_tab_columns#{db_link}
|
37
|
+
where owner = '#{owner}'
|
38
|
+
and table_name = '#{desc_table_name}'
|
39
|
+
order by column_id
|
40
|
+
SQL
|
41
|
+
|
42
|
+
raw_geom_infos = column_spatial_info(desc_table_name)
|
43
|
+
|
44
|
+
# added deletion of ignored columns
|
45
|
+
select_all(table_cols, name).delete_if do |row|
|
46
|
+
ignored_columns && ignored_columns.include?(row['name'].downcase)
|
47
|
+
end.map do |row|
|
48
|
+
limit, scale = row['limit'], row['scale']
|
49
|
+
if limit || scale
|
50
|
+
row['sql_type'] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
|
51
|
+
end
|
52
|
+
|
53
|
+
# clean up odd default spacing from Oracle
|
54
|
+
if row['data_default']
|
55
|
+
row['data_default'].sub!(/^(.*?)\s*$/, '\1')
|
56
|
+
|
57
|
+
# If a default contains a newline these cleanup regexes need to
|
58
|
+
# match newlines.
|
59
|
+
row['data_default'].sub!(/^'(.*)'$/m, '\1')
|
60
|
+
row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
|
61
|
+
end
|
62
|
+
|
63
|
+
if row['sql_type'] =~ /sdo_geometry/i
|
64
|
+
raw_geom_info = raw_geom_infos[oracle_downcase(row['name'])]
|
65
|
+
if(raw_geom_info.nil?)
|
66
|
+
puts "Couldn't find geom info for #{row['name']} in #{raw_geom_infos.inspect}"
|
67
|
+
end
|
68
|
+
ActiveRecord::ConnectionAdapters::SpatialOracleColumn.new(
|
69
|
+
oracle_downcase(row['name']),
|
70
|
+
row['data_default'],
|
71
|
+
raw_geom_info.type,
|
72
|
+
row['nullable'] == 'Y',
|
73
|
+
raw_geom_info.srid,
|
74
|
+
raw_geom_info.with_z,
|
75
|
+
raw_geom_info.with_m)
|
76
|
+
else
|
77
|
+
OracleEnhancedColumn.new(oracle_downcase(row['name']),
|
78
|
+
row['data_default'],
|
79
|
+
row['sql_type'],
|
80
|
+
row['nullable'] == 'Y',
|
81
|
+
# pass table name for table specific column definitions
|
82
|
+
table_name,
|
83
|
+
# pass column type if specified in class definition
|
84
|
+
get_type_for_column(table_name, oracle_downcase(row['name'])))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
ActiveRecord::ConnectionAdapters::OracleEnhancedIndexDefinition.class_eval do
|
93
|
+
def spatial
|
94
|
+
type == "MDSYS.SPATIAL_INDEX"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
|
99
|
+
|
100
|
+
include SpatialAdapter
|
101
|
+
|
102
|
+
SPATIAL_TOLERANCE = 0.5
|
103
|
+
|
104
|
+
def supports_geographic?
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def default_srid
|
109
|
+
8307
|
110
|
+
end
|
111
|
+
|
112
|
+
alias :original_native_database_types :native_database_types
|
113
|
+
def native_database_types
|
114
|
+
types = original_native_database_types.merge!(geometry_data_types)
|
115
|
+
|
116
|
+
# Change mapping of :float from NUMBER to FLOAT, as oracle
|
117
|
+
# recognizes the ansi keyword and it helps keep the schema
|
118
|
+
# stable, even though it ends up mapping to the same underlying
|
119
|
+
# native type.
|
120
|
+
types[:float] = {:name => "FLOAT", :limit => 126 }
|
121
|
+
|
122
|
+
types
|
123
|
+
end
|
124
|
+
|
125
|
+
#Redefines the quote method to add behaviour for when a Geometry is encountered ; used when binding variables in find_by methods
|
126
|
+
def quote(value, column = nil)
|
127
|
+
if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
|
128
|
+
quote_generic_geom( value )
|
129
|
+
elsif value && column && [:text, :binary].include?(column.type)
|
130
|
+
%Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def quote_point_geom( value )
|
137
|
+
if !value.with_z && !value.with_m
|
138
|
+
"SDO_GEOMETRY( 2001, #{value.srid}, MDSYS.SDO_POINT_TYPE( #{value.x}, #{value.y}, NULL ), NULL, NULL )"
|
139
|
+
elsif value.with_z && value.with_m
|
140
|
+
raise ArgumentError, "4d points not currently supported in the oracle_enhanced spatial adapter."
|
141
|
+
#"SDO_GEOMETRY( 2001, #{value.srid}, MDSYS.SDO_POINT_TYPE( #{value.x}, #{value.y}, #{value.z}, #{value.m} ), NULL, NULL )"
|
142
|
+
elsif value.with_z
|
143
|
+
"SDO_GEOMETRY( 3001, #{value.srid}, MDSYS.SDO_POINT_TYPE( #{value.x}, #{value.y}, #{value.z} ), NULL, NULL )"
|
144
|
+
elsif value.with_m
|
145
|
+
raise ArgumentError, "3d points (with M coord) not currently supported in the oracle_enhanced spatial adapter."
|
146
|
+
#"SDO_GEOMETRY( 2001, #{value.srid}, MDSYS.SDO_POINT_TYPE( #{value.x}, #{value.y}, #{value.m} ), NULL, NULL )"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# This technique only supports 2d geometries
|
151
|
+
def quote_generic_geom( value )
|
152
|
+
if value.kind_of?( GeoRuby::SimpleFeatures::Point )
|
153
|
+
# Small optimization for the most commonly used type: points. Oracle's WKT parsing seems slow
|
154
|
+
quote_point_geom( value )
|
155
|
+
else
|
156
|
+
"SDO_GEOMETRY( '#{value.as_wkt}', #{value.srid} )"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def create_table(name, options = {})
|
161
|
+
table_definition = ActiveRecord::ConnectionAdapters::OracleTableDefinition.new(self)
|
162
|
+
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
|
163
|
+
|
164
|
+
yield table_definition if block_given?
|
165
|
+
|
166
|
+
# table_exists? is slow
|
167
|
+
#if options[:force] && table_exists?(name)
|
168
|
+
if options[:force]
|
169
|
+
drop_table(name) rescue nil
|
170
|
+
end
|
171
|
+
|
172
|
+
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
|
173
|
+
create_sql << "#{name} ("
|
174
|
+
create_sql << table_definition.to_sql
|
175
|
+
create_sql << ") #{options[:options]}"
|
176
|
+
execute create_sql
|
177
|
+
execute "CREATE SEQUENCE #{name}_seq START WITH 10000" unless options[:id] == false
|
178
|
+
|
179
|
+
#added to create the geometric columns identified during the table definition
|
180
|
+
unless table_definition.geom_columns.nil?
|
181
|
+
table_definition.geom_columns.each do |geom_column|
|
182
|
+
geom_column.sql_create_statements(name).each do |stmt|
|
183
|
+
execute stmt
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# We override this for three reasons:
|
190
|
+
# 1) Drop spatial indexes before dropping the tables, as "drop table cascade constraints"
|
191
|
+
# seems to leave metadata around in SDO_INDEX_METADATA_TABLE
|
192
|
+
# 2) Avoid dropping sequences that end in '$', as those are for the spatial indexes (which get dropped above)
|
193
|
+
# 3) Need to clear out USER_SDO_GEOM_METADATA after everything else is done.
|
194
|
+
def structure_drop
|
195
|
+
s = []
|
196
|
+
select_values("select sdo_index_name from user_sdo_index_metadata").uniq.each do |idx|
|
197
|
+
s << "DROP INDEX \"#{idx}\""
|
198
|
+
end
|
199
|
+
|
200
|
+
select_values("select sequence_name from user_sequences order by 1").each do |seq|
|
201
|
+
s << "DROP SEQUENCE \"#{seq}\"" unless seq[-1,1] == '$'
|
202
|
+
end
|
203
|
+
select_values("select table_name from all_tables t
|
204
|
+
where owner = sys_context('userenv','session_user') and secondary='N'
|
205
|
+
and not exists (select mv.mview_name from all_mviews mv where mv.owner = t.owner and mv.mview_name = t.table_name)
|
206
|
+
and not exists (select mvl.log_table from all_mview_logs mvl where mvl.log_owner = t.owner and mvl.log_table = t.table_name)
|
207
|
+
order by 1").each do |table|
|
208
|
+
s << "DROP TABLE \"#{table}\" CASCADE CONSTRAINTS"
|
209
|
+
end
|
210
|
+
s << "DELETE FROM USER_SDO_GEOM_METADATA"
|
211
|
+
join_with_statement_token(s)
|
212
|
+
end
|
213
|
+
|
214
|
+
alias :original_remove_column :remove_column
|
215
|
+
def remove_column(table_name,column_name)
|
216
|
+
columns(table_name).each do |col|
|
217
|
+
if col.name.to_s == column_name.to_s
|
218
|
+
#check if the column is geometric
|
219
|
+
if col.is_a?(SpatialColumn) && col.spatial?
|
220
|
+
execute "DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = '#{table_name.upcase}' AND COLUMN_NAME = '#{column_name.to_s.upcase}'"
|
221
|
+
execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
|
222
|
+
else
|
223
|
+
original_remove_column(table_name,column_name)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
alias :original_add_column :add_column
|
230
|
+
def add_column(table_name, column_name, type, options = {})
|
231
|
+
unless geometry_data_types[type].nil?
|
232
|
+
geom_column = ActiveRecord::ConnectionAdapters::OracleColumnDefinition.new(self,column_name, type, nil,nil,options[:null],options[:srid] || -1 , options[:with_z] || false , options[:with_m] || false)
|
233
|
+
geom_column.sql_create_statements(table_name).each do |stmt|
|
234
|
+
execute stmt
|
235
|
+
end
|
236
|
+
else
|
237
|
+
original_add_column(table_name,column_name,type,options)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
#Adds a spatial index to a column. Its name will be index_<table_name>_on_<column_name> unless the key :name is present in the options hash, in which case its value is taken as the name of the index.
|
242
|
+
def add_index(table_name,column_name,options = {})
|
243
|
+
# invalidate index cache
|
244
|
+
self.all_schema_indexes = nil
|
245
|
+
index_name = options[:name] || "index_#{table_name}_on_#{column_name}"
|
246
|
+
if options[:spatial]
|
247
|
+
execute "CREATE INDEX #{index_name} ON #{table_name} (#{column_name}) INDEXTYPE IS mdsys.spatial_index"
|
248
|
+
else
|
249
|
+
index_type = options[:unique] ? "UNIQUE" : ""
|
250
|
+
#all together
|
251
|
+
execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
def column_spatial_info(table_name)
|
258
|
+
rows = select_all <<-end_sql
|
259
|
+
SELECT column_name, diminfo, srid
|
260
|
+
FROM user_sdo_geom_metadata
|
261
|
+
WHERE table_name = '#{table_name}'
|
262
|
+
end_sql
|
263
|
+
|
264
|
+
raw_geom_infos = {}
|
265
|
+
rows.each do |row|
|
266
|
+
column_name = oracle_downcase(row['column_name'])
|
267
|
+
dims = row['diminfo'].to_ary
|
268
|
+
raw_geom_info = raw_geom_infos[column_name] || ActiveRecord::ConnectionAdapters::RawGeomInfo.new
|
269
|
+
raw_geom_info.type = "geometry"
|
270
|
+
raw_geom_info.with_m = dims.any? {|d| d.sdo_dimname == 'M'}
|
271
|
+
raw_geom_info.with_z = dims.any? {|d| d.sdo_dimname == 'Z'}
|
272
|
+
raw_geom_info.srid = row['srid'].to_i
|
273
|
+
#raw_geom_info.diminfo = row['diminfo']
|
274
|
+
raw_geom_infos[column_name] = raw_geom_info
|
275
|
+
end #constr.each
|
276
|
+
|
277
|
+
raw_geom_infos
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
module ActiveRecord
|
283
|
+
module ConnectionAdapters
|
284
|
+
class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc:
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
module ActiveRecord
|
291
|
+
module ConnectionAdapters
|
292
|
+
class OracleTableDefinition < TableDefinition
|
293
|
+
attr_reader :geom_columns
|
294
|
+
|
295
|
+
def column(name, type, options = {})
|
296
|
+
unless @base.geometry_data_types[type.to_sym].nil?
|
297
|
+
geom_column = OracleColumnDefinition.new(@base, name, type)
|
298
|
+
geom_column.null = options[:null]
|
299
|
+
srid = options[:srid] || -1
|
300
|
+
srid = @base.default_srid if srid == :default_srid
|
301
|
+
geom_column.srid = srid
|
302
|
+
geom_column.with_z = options[:with_z] || false
|
303
|
+
geom_column.with_m = options[:with_m] || false
|
304
|
+
|
305
|
+
@geom_columns = [] if @geom_columns.nil?
|
306
|
+
@geom_columns << geom_column
|
307
|
+
else
|
308
|
+
super(name,type,options)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
SpatialAdapter.geometry_data_types.keys.each do |column_type|
|
313
|
+
class_eval <<-EOV
|
314
|
+
def #{column_type}(*args)
|
315
|
+
options = args.extract_options!
|
316
|
+
column_names = args
|
317
|
+
|
318
|
+
column_names.each { |name| column(name, '#{column_type}', options) }
|
319
|
+
end
|
320
|
+
EOV
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
class OracleColumnDefinition < ColumnDefinition
|
325
|
+
attr_accessor :srid, :with_z,:with_m
|
326
|
+
attr_reader :spatial
|
327
|
+
|
328
|
+
def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil,null=nil,srid=-1,with_z=false,with_m=false)
|
329
|
+
super(base, name, type, limit, default,null)
|
330
|
+
@spatial=true
|
331
|
+
@srid=srid
|
332
|
+
@with_z=with_z
|
333
|
+
@with_m=with_m
|
334
|
+
end
|
335
|
+
|
336
|
+
def sql_create_statements(table_name)
|
337
|
+
type_sql = type_to_sql(type)
|
338
|
+
|
339
|
+
#column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})"
|
340
|
+
column_sql = "ALTER TABLE #{table_name} ADD (#{name} #{type_sql}"
|
341
|
+
column_sql += " NOT NULL" if null == false
|
342
|
+
column_sql += ")"
|
343
|
+
stmts = [column_sql]
|
344
|
+
if srid == 8307 # There are others we should probably support, but this is common
|
345
|
+
dim_elems = ["MDSYS.SDO_DIM_ELEMENT('X', -180.0, 180.0, 0.005)",
|
346
|
+
"MDSYS.SDO_DIM_ELEMENT('Y', -90.0, 90.0, 0.005)"]
|
347
|
+
else
|
348
|
+
dim_elems = ["MDSYS.SDO_DIM_ELEMENT('X', -1000, 1000, 0.005)",
|
349
|
+
"MDSYS.SDO_DIM_ELEMENT('Y', -1000, 1000, 0.005)"]
|
350
|
+
end
|
351
|
+
if @with_z
|
352
|
+
dim_elems << "MDSYS.SDO_DIM_ELEMENT('Z', -1000, 1000, 0.005)"
|
353
|
+
end
|
354
|
+
if @with_m
|
355
|
+
dim_elems << "MDSYS.SDO_DIM_ELEMENT('M', -1000, 1000, 0.005)"
|
356
|
+
end
|
357
|
+
|
358
|
+
stmts << "DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = '#{table_name.to_s.upcase}' AND COLUMN_NAME = '#{name.to_s.upcase}'"
|
359
|
+
stmts << "INSERT INTO USER_SDO_GEOM_METADATA (TABLE_NAME, COLUMN_NAME, DIMINFO, SRID) VALUES (" +
|
360
|
+
"'#{table_name}', '#{name}', MDSYS.SDO_DIM_ARRAY(#{dim_elems.join(',')}),#{srid})"
|
361
|
+
stmts
|
362
|
+
end
|
363
|
+
|
364
|
+
def to_sql(table_name)
|
365
|
+
if @spatial
|
366
|
+
raise "Got here!"
|
367
|
+
else
|
368
|
+
super
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
|
373
|
+
private
|
374
|
+
def type_to_sql(name, limit=nil)
|
375
|
+
case name.to_s
|
376
|
+
when /geometry|point|line_string|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i
|
377
|
+
"MDSYS.SDO_GEOMETRY"
|
378
|
+
else base.type_to_sql(name, limit) rescue name
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
#Would prefer creation of a OracleColumn type instead but I would need to reimplement methods where Column objects are instantiated so I leave it like this
|
388
|
+
module ActiveRecord
|
389
|
+
module ConnectionAdapters
|
390
|
+
class SpatialOracleColumn < Column
|
391
|
+
|
392
|
+
include SpatialColumn
|
393
|
+
|
394
|
+
# With the ruby-oci8 adapter, we get objects back from spatial columns
|
395
|
+
# so this method name is a bit of a misnomer. TODO: change this method name to 'to_geometry'
|
396
|
+
def self.string_to_geometry(obj)
|
397
|
+
return obj unless obj && obj.class.to_s == "OCI8::Object::Mdsys::SdoGeometry"
|
398
|
+
raise "Bad #{obj.class} object: #{obj.inspect}" if obj.sdo_gtype.nil?
|
399
|
+
ndim = obj.sdo_gtype.to_int/1000
|
400
|
+
gtype = obj.sdo_gtype.to_int%1000
|
401
|
+
eleminfo = obj.sdo_elem_info.instance_variable_get('@attributes')
|
402
|
+
ordinates = obj.sdo_ordinates.instance_variable_get('@attributes')
|
403
|
+
case gtype
|
404
|
+
when 1
|
405
|
+
geom = point_from_sdo_geometry(eleminfo, ordinates, ndim, obj.sdo_point)
|
406
|
+
when 2
|
407
|
+
geom = linestrings_from_sdo_geometry(eleminfo, ordinates, ndim)[0]
|
408
|
+
when 3
|
409
|
+
geom = polygons_from_sdo_geometry(eleminfo, ordinates, ndim)[0]
|
410
|
+
when 4
|
411
|
+
geom = geomcollection_from_sdo_geometry(eleminfo, ordinates, ndim)
|
412
|
+
when 5
|
413
|
+
geom = MultiPoint.from_coordinates(group_ordinates(ordinates, ndim))
|
414
|
+
when 6
|
415
|
+
linestrings = linestrings_from_sdo_geometry(eleminfo, ordinates, ndim)
|
416
|
+
geom = MultiLineString.from_line_strings(linestrings)
|
417
|
+
when 7
|
418
|
+
polygons = polygons_from_sdo_geometry(eleminfo, ordinates, ndim)
|
419
|
+
geom = MultiPolygon.from_polygons(polygons)
|
420
|
+
else
|
421
|
+
raise "Unhandled geometry type #{obj.sdo_gtype.to_int}"
|
422
|
+
end
|
423
|
+
geom.srid = obj.sdo_srid.to_i
|
424
|
+
geom
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.group_ordinates(ordinates, num_dims)
|
428
|
+
coords = []
|
429
|
+
ordinates.each_slice(num_dims) do |coord|
|
430
|
+
x,y = coord
|
431
|
+
coords << [x.to_f, y.to_f]
|
432
|
+
end
|
433
|
+
coords
|
434
|
+
end
|
435
|
+
|
436
|
+
def self.point_from_sdo_point(sdo_point, ndim)
|
437
|
+
if ndim == 2
|
438
|
+
Point.from_x_y(sdo_point.x.to_f, sdo_point.y.to_f)
|
439
|
+
elsif ndim == 3
|
440
|
+
Point.from_x_y_z(sdo_point.x.to_f, sdo_point.y.to_f, sdo_point.z.to_f)
|
441
|
+
else
|
442
|
+
raise "Not supporting #{ndim} dimensional points"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def self.point_from_sdo_geometry(elem_info, ordinates, ndim, sdo_point)
|
447
|
+
if sdo_point
|
448
|
+
point_from_sdo_point(sdo_point, ndim)
|
449
|
+
else
|
450
|
+
coords = ordinates.slice(0,ndim)
|
451
|
+
if ndim == 2
|
452
|
+
Point.from_x_y(ordinates[0].to_f, ordinates[1].to_f)
|
453
|
+
elsif ndim == 3
|
454
|
+
Point.from_x_y_z(ordinates[0].to_f, ordinates[1].to_f, ordinates[2].to_f)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def self.linestrings_from_sdo_geometry(elem_info, ordinates, ndim)
|
460
|
+
num_ls = elem_info.size / 3
|
461
|
+
linestrings = []
|
462
|
+
0.upto(num_ls-1) do |ring_idx|
|
463
|
+
(start_ord, ring_type, gtype, end_ord) = elem_info.slice(ring_idx*3,4)
|
464
|
+
end_ord ||= ordinates.size + 1
|
465
|
+
num_ords = end_ord - start_ord
|
466
|
+
linestrings << LineString.from_coordinates(group_ordinates(ordinates.slice(start_ord-1, num_ords), ndim))
|
467
|
+
end
|
468
|
+
linestrings
|
469
|
+
end
|
470
|
+
|
471
|
+
def self.polygons_from_sdo_geometry(elem_info, ordinates, ndim)
|
472
|
+
num_rings = elem_info.size / 3
|
473
|
+
polygons = []
|
474
|
+
rings = []
|
475
|
+
cur_polygon = nil
|
476
|
+
0.upto(num_rings-1) do |ring_idx|
|
477
|
+
(start_ord, etype, interpretation, end_ord) = elem_info.slice(ring_idx*3,4)
|
478
|
+
end_ord ||= ordinates.size + 1
|
479
|
+
num_ords = end_ord - start_ord
|
480
|
+
if etype == 1003 && rings.size > 0 # exterior ring
|
481
|
+
polygons << Polygon.from_linear_rings(rings)
|
482
|
+
rings = []
|
483
|
+
end
|
484
|
+
rings << LinearRing.from_coordinates(group_ordinates(ordinates.slice(start_ord-1, num_ords), ndim))
|
485
|
+
end
|
486
|
+
if rings.size > 0 # exterior ring
|
487
|
+
polygons << Polygon.from_linear_rings(rings)
|
488
|
+
rings = []
|
489
|
+
end
|
490
|
+
polygons
|
491
|
+
end
|
492
|
+
|
493
|
+
def self.geomcollection_from_sdo_geometry(elem_info, ordinates, ndim)
|
494
|
+
num_elems = elem_info.size / 3
|
495
|
+
geometries = []
|
496
|
+
0.upto(num_elems-1) do |idx|
|
497
|
+
(start_ord, etype, interpretation, end_ord) = elem_info.slice(idx*3,4)
|
498
|
+
end_ord ||= ordinates.size + 1
|
499
|
+
num_ords = end_ord - start_ord
|
500
|
+
geom = nil
|
501
|
+
case etype
|
502
|
+
when 1
|
503
|
+
if ndim == 2
|
504
|
+
geom = Point.from_x_y(ordinates[start_ord-1].to_f, ordinates[start_ord].to_f)
|
505
|
+
elsif ndim == 3
|
506
|
+
geom = Point.from_x_y_z(ordinates[start_ord-1].to_f, ordinates[start_ord].to_f, ordinates[start_ord+1].to_f)
|
507
|
+
else
|
508
|
+
raise "Not supporting #{ndim} dimensional points"
|
509
|
+
end
|
510
|
+
when 2
|
511
|
+
geom = LineString.from_coordinates(group_ordinates(ordinates.slice(start_ord-1, num_ords), ndim))
|
512
|
+
when 1003
|
513
|
+
raise "Unsupported interpretation #{interpretation} for etype 1003" if interpretation != 1
|
514
|
+
ring = LinearRing.from_coordinates(group_ordinates(ordinates.slice(start_ord-1, num_ords), ndim))
|
515
|
+
geom = Polygon.from_linear_rings([ring])
|
516
|
+
when 2003
|
517
|
+
raise "Unsupported interpretation #{interpretation} for etype 2003" if interpretation != 1
|
518
|
+
ring = LinearRing.from_coordinates(group_ordinates(ordinates.slice(start_ord-1, num_ords), ndim))
|
519
|
+
last_geom = geometries.pop
|
520
|
+
geom = Polygon.from_linear_rings(last_geom.rings + [ring])
|
521
|
+
else
|
522
|
+
raise "Unsupported type in collection: etype = #{etype}, interpretation = #{interpretation}"
|
523
|
+
end
|
524
|
+
geometries << geom if geom
|
525
|
+
end
|
526
|
+
GeometryCollection.from_geometries(geometries)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
ActiveRecord::SchemaDumper.class_eval do
|
533
|
+
def oracle_enhanced_table(table, stream)
|
534
|
+
columns = @connection.columns(table)
|
535
|
+
begin
|
536
|
+
tbl = StringIO.new
|
537
|
+
|
538
|
+
# first dump primary key column
|
539
|
+
if @connection.respond_to?(:pk_and_sequence_for)
|
540
|
+
pk, pk_seq = @connection.pk_and_sequence_for(table)
|
541
|
+
elsif @connection.respond_to?(:primary_key)
|
542
|
+
pk = @connection.primary_key(table)
|
543
|
+
end
|
544
|
+
|
545
|
+
tbl.print " create_table #{table.inspect}"
|
546
|
+
|
547
|
+
# addition to make temporary option work
|
548
|
+
tbl.print ", :temporary => true" if @connection.temporary_table?(table)
|
549
|
+
|
550
|
+
if columns.detect { |c| c.name == pk }
|
551
|
+
if pk != 'id'
|
552
|
+
tbl.print %Q(, :primary_key => "#{pk}")
|
553
|
+
end
|
554
|
+
else
|
555
|
+
tbl.print ", :id => false"
|
556
|
+
end
|
557
|
+
tbl.print ", :force => true"
|
558
|
+
tbl.puts " do |t|"
|
559
|
+
|
560
|
+
# then dump all non-primary key columns
|
561
|
+
column_specs = columns.map do |column|
|
562
|
+
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
|
563
|
+
next if column.name == pk
|
564
|
+
spec = column_spec(column)
|
565
|
+
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
|
566
|
+
spec
|
567
|
+
end.compact
|
568
|
+
|
569
|
+
# find all migration keys used in this table
|
570
|
+
keys = [:name, :limit, :precision, :scale, :default, :null, :with_z, :with_m, :srid] & column_specs.map(&:keys).flatten
|
571
|
+
|
572
|
+
# figure out the lengths for each column based on above keys
|
573
|
+
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
|
574
|
+
|
575
|
+
# the string we're going to sprintf our values against, with standardized column widths
|
576
|
+
format_string = lengths.map{ |len| "%-#{len}s" }
|
577
|
+
|
578
|
+
# find the max length for the 'type' column, which is special
|
579
|
+
type_length = column_specs.map{ |column| column[:type].length }.max
|
580
|
+
|
581
|
+
# add column type definition to our format string
|
582
|
+
format_string.unshift " t.%-#{type_length}s "
|
583
|
+
|
584
|
+
format_string *= ''
|
585
|
+
|
586
|
+
column_specs.each do |colspec|
|
587
|
+
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
|
588
|
+
values.unshift colspec[:type]
|
589
|
+
tbl.print((format_string % values).gsub(/,\s*$/, ''))
|
590
|
+
tbl.puts
|
591
|
+
end
|
592
|
+
|
593
|
+
tbl.puts " end"
|
594
|
+
tbl.puts
|
595
|
+
|
596
|
+
indexes(table, tbl)
|
597
|
+
|
598
|
+
tbl.rewind
|
599
|
+
stream.print tbl.read
|
600
|
+
rescue => e
|
601
|
+
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
602
|
+
stream.puts "# #{e.message} #{e.backtrace.join("\n")}"
|
603
|
+
stream.puts
|
604
|
+
end
|
605
|
+
|
606
|
+
stream
|
607
|
+
end
|
608
|
+
|
609
|
+
private
|
610
|
+
|
611
|
+
def indexes_with_oracle_enhanced_spatial(table, stream)
|
612
|
+
if (indexes = @connection.indexes(table)).any?
|
613
|
+
add_index_statements = indexes.map do |index|
|
614
|
+
case index.type
|
615
|
+
when nil
|
616
|
+
# use table.inspect as it will remove prefix and suffix
|
617
|
+
statement_parts = [ ('add_index ' + table.inspect) ]
|
618
|
+
statement_parts << index.columns.inspect
|
619
|
+
statement_parts << (':name => ' + index.name.inspect)
|
620
|
+
statement_parts << ':unique => true' if index.unique
|
621
|
+
statement_parts << ':tablespace => ' + index.tablespace.inspect if index.tablespace
|
622
|
+
when 'MDSYS.SPATIAL_INDEX'
|
623
|
+
statement_parts = [ ('add_index ' + table.inspect) ]
|
624
|
+
statement_parts << index.columns.inspect
|
625
|
+
statement_parts << (':name => ' + index.name.inspect)
|
626
|
+
statement_parts << ':unique => true' if index.unique
|
627
|
+
statement_parts << ':spatial => true' if index.spatial
|
628
|
+
statement_parts << ':tablespace => ' + index.tablespace.inspect if index.tablespace
|
629
|
+
when 'CTXSYS.CONTEXT'
|
630
|
+
if index.statement_parameters
|
631
|
+
statement_parts = [ ('add_context_index ' + table.inspect) ]
|
632
|
+
statement_parts << index.statement_parameters
|
633
|
+
else
|
634
|
+
statement_parts = [ ('add_context_index ' + table.inspect) ]
|
635
|
+
statement_parts << index.columns.inspect
|
636
|
+
statement_parts << (':name => ' + index.name.inspect)
|
637
|
+
end
|
638
|
+
else
|
639
|
+
# unrecognized index type
|
640
|
+
statement_parts = ["# unrecognized index #{index.name.inspect} with type #{index.type.inspect}"]
|
641
|
+
end
|
642
|
+
' ' + statement_parts.join(', ')
|
643
|
+
end
|
644
|
+
|
645
|
+
stream.puts add_index_statements.sort.join("\n")
|
646
|
+
stream.puts
|
647
|
+
end
|
648
|
+
end
|
649
|
+
alias_method_chain :indexes, :oracle_enhanced_spatial
|
650
|
+
|
651
|
+
end
|