activerecord-postgresql-extensions 0.0.12 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -2
  5. data/Rakefile +4 -17
  6. data/activerecord-postgresql-extensions.gemspec +13 -57
  7. data/lib/{postgresql_extensions/postgresql_adapter_extensions.rb → active_record/postgresql_extensions/adapter_extensions.rb} +44 -46
  8. data/lib/{postgresql_extensions/postgresql_constraints.rb → active_record/postgresql_extensions/constraints.rb} +121 -10
  9. data/lib/{postgresql_extensions/postgresql_extensions.rb → active_record/postgresql_extensions/extensions.rb} +1 -1
  10. data/lib/{postgresql_extensions → active_record/postgresql_extensions}/foreign_key_associations.rb +9 -1
  11. data/lib/{postgresql_extensions/postgresql_functions.rb → active_record/postgresql_extensions/functions.rb} +9 -3
  12. data/lib/{postgresql_extensions/postgresql_geometry.rb → active_record/postgresql_extensions/geometry.rb} +111 -35
  13. data/lib/{postgresql_extensions/postgresql_indexes.rb → active_record/postgresql_extensions/indexes.rb} +4 -2
  14. data/lib/{postgresql_extensions/postgresql_languages.rb → active_record/postgresql_extensions/languages.rb} +1 -1
  15. data/lib/{postgresql_extensions/postgresql_permissions.rb → active_record/postgresql_extensions/permissions.rb} +3 -3
  16. data/lib/active_record/postgresql_extensions/postgis.rb +53 -0
  17. data/lib/{postgresql_extensions/postgresql_roles.rb → active_record/postgresql_extensions/roles.rb} +1 -1
  18. data/lib/{postgresql_extensions/postgresql_rules.rb → active_record/postgresql_extensions/rules.rb} +3 -3
  19. data/lib/{postgresql_extensions/postgresql_schemas.rb → active_record/postgresql_extensions/schemas.rb} +1 -1
  20. data/lib/{postgresql_extensions/postgresql_sequences.rb → active_record/postgresql_extensions/sequences.rb} +2 -2
  21. data/lib/{postgresql_extensions/postgresql_tables.rb → active_record/postgresql_extensions/tables.rb} +18 -4
  22. data/lib/{postgresql_extensions/postgresql_tablespaces.rb → active_record/postgresql_extensions/tablespaces.rb} +1 -1
  23. data/lib/{postgresql_extensions/postgresql_text_search.rb → active_record/postgresql_extensions/text_search.rb} +3 -3
  24. data/lib/{postgresql_extensions/postgresql_triggers.rb → active_record/postgresql_extensions/triggers.rb} +1 -1
  25. data/lib/{postgresql_extensions/postgresql_types.rb → active_record/postgresql_extensions/types.rb} +1 -1
  26. data/lib/active_record/postgresql_extensions/utils.rb +23 -0
  27. data/lib/active_record/postgresql_extensions/version.rb +7 -0
  28. data/lib/{postgresql_extensions/postgresql_views.rb → active_record/postgresql_extensions/views.rb} +2 -2
  29. data/lib/activerecord-postgresql-extensions.rb +23 -22
  30. data/test/adapter_tests.rb +9 -9
  31. data/test/constraints_tests.rb +155 -0
  32. data/test/database.yml +17 -0
  33. data/test/geometry_tests.rb +224 -52
  34. data/test/index_tests.rb +16 -1
  35. data/test/rules_tests.rb +4 -4
  36. data/test/sequences_tests.rb +0 -22
  37. data/test/tables_tests.rb +28 -31
  38. data/test/test_helper.rb +70 -23
  39. data/test/trigger_tests.rb +5 -5
  40. metadata +112 -25
  41. data/VERSION +0 -1
@@ -3,7 +3,7 @@ require 'active_record/connection_adapters/postgresql_adapter'
3
3
 
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
- class PostgreSQLAdapter < AbstractAdapter
6
+ class PostgreSQLAdapter
7
7
  # Creates a new PostgreSQL text search configuration. You must provide
8
8
  # either a parser_name or a source_config option as per the PostgreSQL
9
9
  # text search docs.
@@ -1,5 +1,5 @@
1
1
 
2
- module PostgreSQLExtensions::ActiveRecord
2
+ module ActiveRecord::PostgreSQLExtensions
3
3
  # The ForeignKeyAssociations module attempts to automatically create
4
4
  # associations based on your database schema by looking at foreign key
5
5
  # relationships. It can be enabled by setting the
@@ -365,3 +365,11 @@ ActiveRecord::Base.class_eval do
365
365
  # convention of "FirstModelSecondModel".
366
366
  cattr_accessor :strict_foreign_key_has_many_throughs
367
367
  end
368
+
369
+ # for backwards compatibility with the older module name.
370
+ module PostgreSQLExtensions
371
+ module ActiveRecord
372
+ ForeignKeyAssociations = ::ActiveRecord::PostgreSQLExtensions::ForeignKeyAssociations
373
+ end
374
+ end
375
+
@@ -27,7 +27,7 @@ module ActiveRecord
27
27
  end
28
28
 
29
29
  module ConnectionAdapters
30
- class PostgreSQLAdapter < AbstractAdapter
30
+ class PostgreSQLAdapter
31
31
  # Creates a PostgreSQL function/stored procedure.
32
32
  #
33
33
  # +args+ is a simple String that you can use to represent the
@@ -306,10 +306,16 @@ module ActiveRecord
306
306
 
307
307
  private
308
308
  def build_statement(k, v) #:nodoc:
309
- sql = "ALTER FUNCTION #{base.quote_function(@new_name || name)}(#{args}) "
309
+ new_name = if defined?(@new_name) && @new_name
310
+ @new_name
311
+ else
312
+ self.name
313
+ end
314
+
315
+ sql = "ALTER FUNCTION #{base.quote_function(new_name)}(#{args}) "
310
316
  sql << case k
311
317
  when :rename_to
312
- "RENAME TO #{base.quote_generic_ignore_schema(v)}".tap { @new_name = v }
318
+ "RENAME TO #{base.quote_generic_ignore_scoped_schema(v)}".tap { @new_name = v }
313
319
  when :owner_to
314
320
  "OWNER TO #{base.quote_role(v)}"
315
321
  when :set_schema
@@ -1,5 +1,6 @@
1
1
 
2
2
  require 'active_record/connection_adapters/postgresql_adapter'
3
+ require 'active_record/postgresql_extensions/postgis'
3
4
 
4
5
  module ActiveRecord
5
6
  class InvalidGeometryType < ActiveRecordError #:nodoc:
@@ -8,46 +9,71 @@ module ActiveRecord
8
9
  end
9
10
  end
10
11
 
12
+ class InvalidSpatialColumnType < ActiveRecordError #:nodoc:
13
+ def initialize(type)
14
+ super("Invalid PostGIS spatial column type - #{type}")
15
+ end
16
+ end
17
+
11
18
  class InvalidGeometryDimensions < ActiveRecordError #:nodoc:
12
19
  end
13
20
 
14
21
  module ConnectionAdapters
15
- class PostgreSQLAdapter < AbstractAdapter
16
- def native_database_types_with_geometry #:nodoc:
17
- native_database_types_without_geometry.merge({
18
- :geometry => { :name => 'geometry' }
22
+ class PostgreSQLAdapter
23
+ def native_database_types_with_spatial_types #:nodoc:
24
+ native_database_types_without_spatial_types.merge({
25
+ :geometry => { :name => 'geometry' },
26
+ :geography => { :name => 'geography' }
19
27
  })
20
28
  end
21
- alias_method_chain :native_database_types, :geometry
29
+ alias_method_chain :native_database_types, :spatial_types
22
30
  end
23
31
 
24
- class PostgreSQLTableDefinition < TableDefinition
25
- attr_reader :geometry_columns
32
+ class PostgreSQLGeometryColumnDefinition
33
+ end
26
34
 
27
- # This is a special geometry type for the PostGIS extension's
28
- # geometry data type. It is used in a table definition to define
29
- # a geometry column.
35
+ class PostgreSQLTableDefinition < TableDefinition
36
+ # This is a special spatial type for the PostGIS extension's
37
+ # data types. It is used in a table definition to define
38
+ # a spatial column.
39
+ #
40
+ # Depending on the version of PostGIS being used, we'll try to create
41
+ # geometry columns in a post-2.0-ish, typmod-based way or a pre-2.0-ish
42
+ # AddGeometryColumn-based way. We can also add CHECK constraints and
43
+ # create a GiST index on the column all in one go.
30
44
  #
31
- # Essentially this method works like a wrapper around the PostGIS
32
- # AddGeometryColumn function. It can create the geometry column,
33
- # add CHECK constraints and create a GiST index on the column
34
- # all in one go.
45
+ # In versions of PostGIS prior to 2.0, geometry columns are created using
46
+ # the AddGeometryColumn and will created with CHECK constraints where
47
+ # appropriate and entries to the <tt>geometry_columns</tt> will be
48
+ # updated accordingly.
49
+ #
50
+ # In versions of PostGIS after 2.0, geometry columns are creating using
51
+ # typmod specifiers. CHECK constraints can still be created, but their
52
+ # creation must be forced using the <tt>:force_constraints</tt> option.
53
+ #
54
+ # The <tt>geometry</tt> and <tt>geography</tt> methods are shortcuts to
55
+ # calling the <tt>spatial</tt> method with the <tt>:spatial_column_type</tt>
56
+ # option set accordingly.
35
57
  #
36
58
  # ==== Options
37
59
  #
60
+ # * <tt>:spatial_column_type</tt> - the column type. This value can
61
+ # be one of <tt>:geometry</tt> or <tt>:geography</tt>. This value
62
+ # doesn't refer to the spatial type used by the column, but rather
63
+ # by the actual column type itself.
38
64
  # * <tt>:geometry_type</tt> - set the geometry type. The actual
39
- # data type is always "geometry"; this option is used in the
40
- # <tt>geometry_columns</tt> table and on the CHECK constraints
41
- # to enforce the geometry type allowed in the field. The default
42
- # is "GEOMETRY". See the PostGIS documentation for valid types,
43
- # or check out the GEOMETRY_TYPES constant in this extension.
65
+ # data type is either "geometry" or "geography"; this option refers to
66
+ # the spatial type being used.
44
67
  # * <tt>:add_constraints</tt> - automatically creates the CHECK
45
68
  # constraints used to enforce ndims, srid and geometry type.
46
69
  # The default is true.
70
+ # * <tt>:force_constraints</tt> - forces the creation of CHECK
71
+ # constraints in versions of PostGIS post-2.0.
47
72
  # * <tt>:add_geometry_columns_entry</tt> - automatically adds
48
73
  # an entry to the <tt>geometry_columns</tt> table. We will
49
74
  # try to delete any existing match in <tt>geometry_columns</tt>
50
- # before inserting. The default is true.
75
+ # before inserting. The default is true. This value is ignored in
76
+ # versions of PostGIS post-2.0.
51
77
  # * <tt>:create_gist_index</tt> - automatically creates a GiST
52
78
  # index for the new geometry column. This option accepts either
53
79
  # a true/false expression or a String. If the value is a String,
@@ -58,15 +84,20 @@ module ActiveRecord
58
84
  # <tt>:geometry_type</tt> ends in an "m" (for "measured
59
85
  # geometries" the default is 3); for everything else, it is 2.
60
86
  # * <tt>:srid</tt> - the SRID, a.k.a. the Spatial Reference
61
- # Identifier. The default is -1, which is a special SRID
62
- # PostGIS in lieu of a real SRID.
63
- def geometry(column_name, opts = {})
87
+ # Identifier. The default depends on the version of PostGIS being used
88
+ # and the spatial column type being used. Refer to the PostGIS docs
89
+ # for the specifics, but generally this means either a value of -1
90
+ # for versions of PostGIS prior to 2.0 for geometry columns and a value
91
+ # of 0 for versions post-2.0 and for all geography columns.
92
+ def spatial(column_name, opts = {})
64
93
  opts = {
94
+ :spatial_column_type => :geometry,
65
95
  :geometry_type => :geometry,
66
96
  :add_constraints => true,
97
+ :force_constraints => false,
67
98
  :add_geometry_columns_entry => true,
68
99
  :create_gist_index => true,
69
- :srid => -1
100
+ :srid => ActiveRecord::PostgreSQLExtensions::PostGIS.UNKNOWN_SRID
70
101
  }.merge(opts)
71
102
 
72
103
  if opts[:ndims].blank?
@@ -77,25 +108,41 @@ module ActiveRecord
77
108
  end
78
109
  end
79
110
 
111
+ assert_valid_spatial_column_type(opts[:spatial_column_type])
80
112
  assert_valid_geometry_type(opts[:geometry_type])
81
113
  assert_valid_ndims(opts[:ndims], opts[:geometry_type])
82
114
 
83
- column = self[column_name] || ColumnDefinition.new(base, column_name, :geometry)
115
+ column_type = if ActiveRecord::PostgreSQLExtensions::PostGIS.VERSION[:lib] < '2.0'
116
+ opts[:spatial_column_type]
117
+ else
118
+ column_args = [ opts[:geometry_type].to_s.upcase ]
119
+
120
+ if ![ 0, -1 ].include?(opts[:srid])
121
+ column_args << opts[:srid]
122
+ end
123
+
124
+ "#{opts[:spatial_column_type]}(#{column_args.join(', ')})"
125
+ end
126
+
127
+ column = self[column_name] || ColumnDefinition.new(base, column_name, column_type)
84
128
  column.default = opts[:default]
85
129
  column.null = opts[:null]
86
130
 
87
131
  unless @columns.include?(column)
88
132
  @columns << column
89
- if opts[:add_constraints]
133
+ if opts[:add_constraints] && (
134
+ ActiveRecord::PostgreSQLExtensions::PostGIS.VERSION[:lib] < '2.0' ||
135
+ opts[:force_constraints]
136
+ )
90
137
  @table_constraints << PostgreSQLCheckConstraint.new(
91
138
  base,
92
- "srid(#{base.quote_column_name(column_name)}) = (#{opts[:srid].to_i})",
139
+ "ST_srid(#{base.quote_column_name(column_name)}) = (#{opts[:srid].to_i})",
93
140
  :name => "enforce_srid_#{column_name}"
94
141
  )
95
142
 
96
143
  @table_constraints << PostgreSQLCheckConstraint.new(
97
144
  base,
98
- "ndims(#{base.quote_column_name(column_name)}) = #{opts[:ndims].to_i}",
145
+ "ST_ndims(#{base.quote_column_name(column_name)}) = #{opts[:ndims].to_i}",
99
146
  :name => "enforce_dims_#{column_name}"
100
147
  )
101
148
 
@@ -111,10 +158,10 @@ module ActiveRecord
111
158
 
112
159
  # We want to split up the schema and the table name for the
113
160
  # upcoming geometry_columns rows and GiST index.
114
- current_schema, current_table_name = if self.table_name.is_a?(Hash)
161
+ current_scoped_schema, current_table_name = if self.table_name.is_a?(Hash)
115
162
  [ self.table_name.keys.first, self.table_name.values.first ]
116
- elsif base.current_schema
117
- [ base.current_schema, self.table_name ]
163
+ elsif base.current_scoped_schema
164
+ [ base.current_scoped_schema, self.table_name ]
118
165
  else
119
166
  schema, table_name = base.extract_schema_and_table_names(self.table_name)
120
167
  [ schema || 'public', table_name ]
@@ -122,20 +169,23 @@ module ActiveRecord
122
169
 
123
170
  @post_processing ||= Array.new
124
171
 
125
- if opts[:add_geometry_columns_entry]
172
+ if opts[:add_geometry_columns_entry] &&
173
+ opts[:spatial_column_type].to_s != 'geography' &&
174
+ ActiveRecord::PostgreSQLExtensions::PostGIS.VERSION[:lib] < '2.0'
175
+
126
176
  @post_processing << sprintf(
127
177
  "DELETE FROM \"geometry_columns\" WHERE f_table_catalog = '' AND " +
128
178
  "f_table_schema = %s AND " +
129
179
  "f_table_name = %s AND " +
130
180
  "f_geometry_column = %s;",
131
- base.quote(current_schema.to_s),
181
+ base.quote(current_scoped_schema.to_s),
132
182
  base.quote(current_table_name.to_s),
133
183
  base.quote(column_name.to_s)
134
184
  )
135
185
 
136
186
  @post_processing << sprintf(
137
187
  "INSERT INTO \"geometry_columns\" VALUES ('', %s, %s, %s, %d, %d, %s);",
138
- base.quote(current_schema.to_s),
188
+ base.quote(current_scoped_schema.to_s),
139
189
  base.quote(current_table_name.to_s),
140
190
  base.quote(column_name.to_s),
141
191
  opts[:ndims].to_i,
@@ -154,7 +204,7 @@ module ActiveRecord
154
204
  @post_processing << PostgreSQLIndexDefinition.new(
155
205
  base,
156
206
  index_name,
157
- current_table_name,
207
+ { current_scoped_schema => current_table_name },
158
208
  column_name,
159
209
  :using => :gist
160
210
  ).to_s
@@ -163,6 +213,20 @@ module ActiveRecord
163
213
  self
164
214
  end
165
215
 
216
+ def geometry(column_name, opts = {})
217
+ self.spatial(column_name, opts)
218
+ end
219
+
220
+ def geography(column_name, opts = {})
221
+ opts = {
222
+ :srid => ActiveRecord::PostgreSQLExtensions::PostGIS.UNKNOWN_SRIDS[:geography]
223
+ }.merge(opts)
224
+
225
+ self.spatial(column_name, opts.merge(
226
+ :spatial_column_type => :geography
227
+ ))
228
+ end
229
+
166
230
  private
167
231
  GEOMETRY_TYPES = [
168
232
  'GEOMETRY',
@@ -173,6 +237,7 @@ module ActiveRecord
173
237
  'MULTIPOLYGON',
174
238
  'LINESTRING',
175
239
  'MULTILINESTRING',
240
+ 'GEOMETRYM',
176
241
  'GEOMETRYCOLLECTIONM',
177
242
  'POINTM',
178
243
  'MULTIPOINTM',
@@ -192,12 +257,23 @@ module ActiveRecord
192
257
  'MULTISURFACEM'
193
258
  ].freeze
194
259
 
260
+ SPATIAL_COLUMN_TYPES = [
261
+ 'geometry',
262
+ 'geography'
263
+ ].freeze
264
+
195
265
  def assert_valid_geometry_type(type)
196
266
  if !GEOMETRY_TYPES.include?(type.to_s.upcase)
197
267
  raise ActiveRecord::InvalidGeometryType.new(type)
198
268
  end unless type.nil?
199
269
  end
200
270
 
271
+ def assert_valid_spatial_column_type(type)
272
+ if !SPATIAL_COLUMN_TYPES.include?(type.to_s)
273
+ raise ActiveRecord::InvalidSpatialColumnType.new(type)
274
+ end unless type.nil?
275
+ end
276
+
201
277
  def assert_valid_ndims(ndims, type)
202
278
  if !ndims.blank?
203
279
  if type.to_s.upcase =~ /([A-Z]+M)$/ && ndims != 3
@@ -1,4 +1,6 @@
1
1
 
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
2
4
  module ActiveRecord
3
5
  class InvalidIndexColumnDefinition < ActiveRecordError #:nodoc:
4
6
  def initialize(msg, column)
@@ -13,7 +15,7 @@ module ActiveRecord
13
15
  end
14
16
 
15
17
  module ConnectionAdapters
16
- class PostgreSQLAdapter < AbstractAdapter
18
+ class PostgreSQLAdapter
17
19
  # Creates an index. This method is an alternative to the standard
18
20
  # ActiveRecord add_index method and includes PostgreSQL-specific
19
21
  # options.
@@ -179,7 +181,7 @@ module ActiveRecord
179
181
  sql << ')'
180
182
  sql << " WITH (FILLFACTOR = #{options[:fill_factor].to_i})" if options[:fill_factor]
181
183
  sql << " TABLESPACE #{base.quote_tablespace(options[:tablespace])}" if options[:tablespace]
182
- sql << " WHERE #{options[:conditions] || options[:where]}" if options[:conditions] || options[:where]
184
+ sql << " WHERE (#{options[:conditions] || options[:where]})" if options[:conditions] || options[:where]
183
185
  "#{sql};"
184
186
  end
185
187
  alias :to_s :to_sql
@@ -3,7 +3,7 @@ require 'active_record/connection_adapters/postgresql_adapter'
3
3
 
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
- class PostgreSQLAdapter < AbstractAdapter
6
+ class PostgreSQLAdapter
7
7
  # Creates a PostgreSQL procedural language.
8
8
  #
9
9
  # Note that you can grant privileges on languages using the
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  module ConnectionAdapters
12
- class PostgreSQLAdapter < AbstractAdapter
12
+ class PostgreSQLAdapter
13
13
  # Grants privileges on tables. You can specify multiple tables,
14
14
  # roles and privileges all at once using Arrays for each of the
15
15
  # desired parameters. See PostgreSQLGrantPrivilege for
@@ -224,7 +224,7 @@ module ActiveRecord
224
224
  sql << Array(objects).collect do |t|
225
225
  if my_query_options[:quote_objects]
226
226
  if my_query_options[:ignore_schema]
227
- base.quote_generic_ignore_schema(t)
227
+ base.quote_generic_ignore_scoped_schema(t)
228
228
  else
229
229
  base.quote_table_name(t)
230
230
  end
@@ -293,7 +293,7 @@ module ActiveRecord
293
293
  sql << Array(objects).collect do |t|
294
294
  if my_query_options[:quote_objects]
295
295
  if my_query_options[:ignore_schema]
296
- base.quote_generic_ignore_schema(t)
296
+ base.quote_generic_ignore_scoped_schema(t)
297
297
  else
298
298
  base.quote_table_name(t)
299
299
  end
@@ -0,0 +1,53 @@
1
+
2
+ module ActiveRecord
3
+ module PostgreSQLExtensions
4
+ module PostGIS
5
+ class << self
6
+ def VERSION
7
+ return @VERSION if defined?(@VERSION)
8
+
9
+ @VERSION = if (version_string = ::ActiveRecord::Base.connection.select_rows("SELECT postgis_full_version()").flatten.first).present?
10
+ hash = {
11
+ :use_stats => version_string =~ /USE_STATS/
12
+ }
13
+
14
+ {
15
+ :lib => /POSTGIS="([^"]+)"/,
16
+ :geos => /GEOS="([^"]+)"/,
17
+ :proj => /PROJ="([^"]+)"/,
18
+ :libxml => /LIBXML="([^"]+)"/
19
+ }.each do |k, v|
20
+ hash[k] = version_string.scan(v).flatten.first
21
+ end
22
+
23
+ hash.freeze
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def UNKNOWN_SRIDS
30
+ return @UNKNOWN_SRIDS if defined?(@UNKNOWN_SRIDS)
31
+
32
+ @UNKNOWN_SRIDS = if self.VERSION[:lib] >= '2.0'
33
+ {
34
+ :geography => 0,
35
+ :geometry => 0
36
+ }.freeze
37
+ else
38
+ {
39
+ :geography => 0,
40
+ :geometry => -1
41
+ }.freeze
42
+ end
43
+ end
44
+
45
+ def UNKNOWN_SRID
46
+ return @UNKNOWN_SRID if defined?(@UNKNOWN_SRID)
47
+
48
+ @UNKNOWN_SRID = self.UNKNOWN_SRIDS[:geometry]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  module ConnectionAdapters
12
- class PostgreSQLAdapter < AbstractAdapter
12
+ class PostgreSQLAdapter
13
13
  def create_role(name, options = {})
14
14
  execute PostgreSQLRole.new(self, :create, name, options).to_sql
15
15
  end