activerecord-postgresql-extensions 0.0.12 → 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.
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