activerecord-postgresql-extensions 0.0.7
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.
- data/MIT-LICENSE +23 -0
- data/README.rdoc +32 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/activerecord-postgresql-extensions.rb +30 -0
- data/lib/postgresql_extensions/foreign_key_associations.rb +367 -0
- data/lib/postgresql_extensions/postgresql_adapter_extensions.rb +646 -0
- data/lib/postgresql_extensions/postgresql_constraints.rb +579 -0
- data/lib/postgresql_extensions/postgresql_functions.rb +345 -0
- data/lib/postgresql_extensions/postgresql_geometry.rb +212 -0
- data/lib/postgresql_extensions/postgresql_indexes.rb +219 -0
- data/lib/postgresql_extensions/postgresql_languages.rb +80 -0
- data/lib/postgresql_extensions/postgresql_permissions.rb +322 -0
- data/lib/postgresql_extensions/postgresql_rules.rb +112 -0
- data/lib/postgresql_extensions/postgresql_schemas.rb +49 -0
- data/lib/postgresql_extensions/postgresql_sequences.rb +222 -0
- data/lib/postgresql_extensions/postgresql_tables.rb +308 -0
- data/lib/postgresql_extensions/postgresql_triggers.rb +131 -0
- data/lib/postgresql_extensions/postgresql_types.rb +17 -0
- data/lib/postgresql_extensions/postgresql_views.rb +103 -0
- data/postgresql-extensions.gemspec +50 -0
- data/test/adapter_test.rb +45 -0
- data/test/constraints_test.rb +98 -0
- data/test/functions_test.rb +112 -0
- data/test/geometry_test.rb +43 -0
- data/test/index_test.rb +68 -0
- data/test/languages_test.rb +48 -0
- data/test/permissions_test.rb +163 -0
- data/test/rules_test.rb +32 -0
- data/test/schemas_test.rb +43 -0
- data/test/sequences_test.rb +90 -0
- data/test/tables_test.rb +49 -0
- data/test/test_helper.rb +64 -0
- metadata +97 -0
@@ -0,0 +1,345 @@
|
|
1
|
+
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class InvalidFunctionBehavior < ActiveRecordError #:nodoc:
|
6
|
+
def initialize(behavior)
|
7
|
+
super("Invalid function behavior - #{behavior}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidFunctionOnNullInputValue < ActiveRecordError #:nodoc:
|
12
|
+
def initialize(option)
|
13
|
+
super("Invalid function ON NULL INPUT behavior - #{option}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidFunctionSecurityValue < ActiveRecordError #:nodoc:
|
18
|
+
def initialize(option)
|
19
|
+
super("Invalid function SECURITY value - #{option}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class InvalidFunctionAction < ActiveRecordError #:nodoc:
|
24
|
+
def initialize(option)
|
25
|
+
super("Invalid function SECURITY value - #{option}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ConnectionAdapters
|
30
|
+
class PostgreSQLAdapter < AbstractAdapter
|
31
|
+
# Creates a PostgreSQL function/stored procedure.
|
32
|
+
#
|
33
|
+
# +args+ is a simple String that you can use to represent the
|
34
|
+
# function arguments.
|
35
|
+
#
|
36
|
+
# +returns+ is the return type for the function.
|
37
|
+
#
|
38
|
+
# +language+ is the procedural language the function is written
|
39
|
+
# in. The possible values for this argument will depend on your
|
40
|
+
# database set up. See create_language for details on adding
|
41
|
+
# new languages to your database.
|
42
|
+
#
|
43
|
+
# +body+ is the actual function body. When the function language is
|
44
|
+
# C, this will be an Array containing two items: the object file
|
45
|
+
# the function is found in and the link symbol for the function.
|
46
|
+
# In all other cases, this argument will be a String containing
|
47
|
+
# the actual function code.
|
48
|
+
#
|
49
|
+
# ==== Options
|
50
|
+
#
|
51
|
+
# * <tt>:force</tt> - add an <tt>OR REPLACE</tt> clause to the
|
52
|
+
# statement, thus overwriting any existing function definition
|
53
|
+
# of the same name and arguments.
|
54
|
+
# * <tt>:behavior</tt> - one of <tt>:immutable</tt>,
|
55
|
+
# <tt>:stable</tt> or <tt>:volatile</tt>. This option helps
|
56
|
+
# the server when making planning estimates when the function
|
57
|
+
# is called. The default is <tt>:volatile</tt>.
|
58
|
+
# * <tt>:on_null_inputs</tt> - one of <tt>:called</tt>,
|
59
|
+
# <tt>:returns</tt> or <tt>:strict</tt>. This indicates to the
|
60
|
+
# server how the function should be called when it receives
|
61
|
+
# NULL inputs. When <tt>:called</tt> is used, the function is
|
62
|
+
# executed even when one or more of its arguments is NULL.
|
63
|
+
# For <tt>:returns</tt> and <tt>:strict</tt> (which are actually
|
64
|
+
# just aliases), function execution is skipped and NULL is
|
65
|
+
# returned immediately.
|
66
|
+
# * <tt>:security</tt> - one of <tt>:invoker</tt> or
|
67
|
+
# <tt>:definer</tt>. This option determines what privileges the
|
68
|
+
# function should used when called. The values are pretty
|
69
|
+
# self explanatory. The default is <tt>:invoker</tt>.
|
70
|
+
# * <tt>:delimiter</tt> - the delimiter to use for the function
|
71
|
+
# body. The default is '$$'.
|
72
|
+
# * <tt>:cost</tt> - a number that determines the approximate
|
73
|
+
# overhead the server can expect when calling this function. This
|
74
|
+
# is used when calculating execution costs in the planner.
|
75
|
+
# * <tt>:rows</tt> - a number indicating the estimated number
|
76
|
+
# of rows the function will return. This is used when
|
77
|
+
# calculating execution costs in the planner and only affects
|
78
|
+
# functions that return result sets.
|
79
|
+
# * <tt>:set</tt> - allows you to set parameters temporarily
|
80
|
+
# during function execution. This would include things like
|
81
|
+
# +search_path+ or <tt>time zone</tt> and such. This option
|
82
|
+
# can either be a String with the set fragment or a Hash
|
83
|
+
# with the parameters as keys and the values to set as values.
|
84
|
+
# When using a Hash, the value <tt>:from_current</tt> can be
|
85
|
+
# used to specify the actual <tt>FROM CURRENT</tt> clause.
|
86
|
+
#
|
87
|
+
# You should definitely check out the PostgreSQL documentation
|
88
|
+
# on creating stored procedures, because it can get pretty
|
89
|
+
# convoluted as evidenced by the plethora of options we're
|
90
|
+
# handling here.
|
91
|
+
#
|
92
|
+
# ==== Example
|
93
|
+
#
|
94
|
+
# ### ruby
|
95
|
+
# create_function('tester_function', 'integer',
|
96
|
+
# 'integer', 'sql', :behavior => :immutable, :set => { :search_path => :from_current }, :force => true) do
|
97
|
+
# "select $1;"
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# # Produces
|
101
|
+
# #
|
102
|
+
# # CREATE OR REPLACE FUNCTION "tester_function"(integer) RETURNS integer AS $$
|
103
|
+
# # select $1;
|
104
|
+
# # $$
|
105
|
+
# # LANGUAGE "sql"
|
106
|
+
# # IMMUTABLE
|
107
|
+
# # SET "search_path" FROM CURRENT;
|
108
|
+
def create_function(name, args, returns, language, options = {})
|
109
|
+
body = yield.to_s
|
110
|
+
execute PostgreSQLFunctionDefinition.new(self, name, args, returns, language, body, options).to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
# Drops a function.
|
114
|
+
#
|
115
|
+
# ==== Options
|
116
|
+
#
|
117
|
+
# * <tt>:if_exists</tt> - adds an <tt>IF EXISTS</tt> clause.
|
118
|
+
# * <tt>:cascade</tt> - cascades the operation on to any objects
|
119
|
+
# referring to the function.
|
120
|
+
def drop_function(name, args, options = {})
|
121
|
+
sql = 'DROP FUNCTION '
|
122
|
+
sql << 'IF EXISTS ' if options[:if_exists]
|
123
|
+
sql << "#{quote_function(name)}(#{args})"
|
124
|
+
sql << ' CASCADE' if options[:cascade]
|
125
|
+
execute sql
|
126
|
+
end
|
127
|
+
|
128
|
+
# Renames a function.
|
129
|
+
def rename_function(name, args, rename_to, options = {})
|
130
|
+
execute PostgreSQLFunctionAlterer.new(self, name, args, :rename_to => rename_to).to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
# Changes the function's owner.
|
134
|
+
def alter_function_owner(name, args, owner_to, options = {})
|
135
|
+
execute PostgreSQLFunctionAlterer.new(self, name, args, :owner_to => owner_to).to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
# Changes the function's schema.
|
139
|
+
def alter_function_schema(name, args, set_schema, options = {})
|
140
|
+
execute PostgreSQLFunctionAlterer.new(self, name, args, :set_schema => set_schema).to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
# Alters a function. There's a ton of stuff you can do here, and
|
144
|
+
# there's two ways to do it: with a block or with an options Hash.
|
145
|
+
#
|
146
|
+
# In both cases, you're going to be using the same options as
|
147
|
+
# defined in <tt>create_function</tt> with the exception of
|
148
|
+
# <tt>:force</tt> and <tt>:delimiter</tt> and with the addition
|
149
|
+
# of <tt>:reset</tt>. The <tt>:reset</tt> option allows you
|
150
|
+
# to reset the values of parameters used with <tt>:set</tt> either
|
151
|
+
# on an individual basis using an Array or by using <tt>:all</tt>
|
152
|
+
# to reset all of them.
|
153
|
+
#
|
154
|
+
# ==== Examples
|
155
|
+
#
|
156
|
+
# Both of the following examples should produce the same output.
|
157
|
+
#
|
158
|
+
# ### ruby
|
159
|
+
# # with options Hash
|
160
|
+
# alter_function('my_function', 'integer', :rename_to => 'another_function')
|
161
|
+
# alter_function('another_function', 'integer', :owner_to => 'jdoe')
|
162
|
+
#
|
163
|
+
# # block mode
|
164
|
+
# alter_function('my_function', 'integer') do |f|
|
165
|
+
# f.rename_to 'another_function'
|
166
|
+
# f.owner_to 'jdoe'
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# # Produces
|
170
|
+
# #
|
171
|
+
# # ALTER FUNCTION "my_function"(integer) OWNER TO "jdoe";
|
172
|
+
# # ALTER FUNCTION "my_function"(integer) RENAME TO "another_function";
|
173
|
+
def alter_function(name, args, options = {})
|
174
|
+
if block_given?
|
175
|
+
alterer = PostgreSQLFunctionAlterer.new(self, name, args)
|
176
|
+
yield alterer
|
177
|
+
execute alterer.to_s unless alterer.empty?
|
178
|
+
else
|
179
|
+
execute PostgreSQLFunctionAlterer.new(self, name, args, options).to_s
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# This is a base class for our PostgreSQL function classes. You don't
|
185
|
+
# want to be accessing this class directly.
|
186
|
+
class PostgreSQLFunction
|
187
|
+
attr_accessor :base, :name, :args, :options
|
188
|
+
|
189
|
+
def initialize(base, name, args, options = {}) #:nodoc:
|
190
|
+
@base, @name, @args, @options = base, name, args, options
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
BEHAVIORS = %w{ immutable stable volatile }.freeze
|
195
|
+
ON_NULL_INPUTS = %w{ called returns strict }.freeze
|
196
|
+
SECURITIES = %w{ invoker definer }.freeze
|
197
|
+
|
198
|
+
def assert_valid_behavior(option) #:nodoc:
|
199
|
+
if !BEHAVIORS.include? option.to_s.downcase
|
200
|
+
raise ActiveRecord::InvalidFunctionBehavior.new(option)
|
201
|
+
end unless option.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
def assert_valid_on_null_input(option) #:nodoc:
|
205
|
+
if !ON_NULL_INPUTS.include? option.to_s.downcase
|
206
|
+
raise ActiveRecord::InvalidFunctionOnNullInputValue.new(option)
|
207
|
+
end unless option.nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
def assert_valid_security(option) #:nodoc:
|
211
|
+
if !SECURITIES.include? option.to_s.downcase
|
212
|
+
raise ActiveRecord::InvalidFunctionSecurityValue.new(option)
|
213
|
+
end unless option.nil?
|
214
|
+
end
|
215
|
+
|
216
|
+
def set_options(opts) #:nodoc:
|
217
|
+
sql = Array.new
|
218
|
+
if opts.is_a?(Hash)
|
219
|
+
opts.each do |k, v|
|
220
|
+
set_me = if k.to_s.upcase == 'TIME ZONE'
|
221
|
+
"SET TIME ZONE #{base.quote_generic(v)}"
|
222
|
+
else
|
223
|
+
"SET #{base.quote_generic(k)}" + if (v == :from_current)
|
224
|
+
" FROM CURRENT"
|
225
|
+
else
|
226
|
+
" TO #{base.quote_generic(v)}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
sql << set_me
|
231
|
+
end
|
232
|
+
else
|
233
|
+
sql << Array(opts).collect do |s|
|
234
|
+
"SET #{s.to_s}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
sql
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Creates a PostgreSQL function definition. You're generally going to
|
242
|
+
# want to use the +create_function+ method instead of accessing this
|
243
|
+
# class directly.
|
244
|
+
class PostgreSQLFunctionDefinition < PostgreSQLFunction
|
245
|
+
attr_accessor :language, :returns, :body
|
246
|
+
|
247
|
+
def initialize(base, name, args, returns, language, body, options = {}) #:nodoc:
|
248
|
+
assert_valid_behavior(options[:behavior])
|
249
|
+
assert_valid_on_null_input(options[:on_null_input])
|
250
|
+
assert_valid_security(options[:security])
|
251
|
+
|
252
|
+
@language, @returns, @body = language, returns, body
|
253
|
+
options = {
|
254
|
+
:delimiter => '$$'
|
255
|
+
}.merge options
|
256
|
+
super(base, name, args, options)
|
257
|
+
end
|
258
|
+
|
259
|
+
def to_sql #:nodoc:
|
260
|
+
sql = 'CREATE '
|
261
|
+
sql << 'OR REPLACE ' if options[:force]
|
262
|
+
sql << "FUNCTION #{base.quote_function(name)}(#{args}) RETURNS #{returns} AS "
|
263
|
+
if language == 'C'
|
264
|
+
"#{base.quote_function(body[0])}, #{base.quote_generic(body[1])}"
|
265
|
+
else
|
266
|
+
sql << "#{options[:delimiter]}\n#{body}\n#{options[:delimiter]}\n"
|
267
|
+
end
|
268
|
+
sql << "LANGUAGE #{base.quote_language(language)}\n"
|
269
|
+
sql << " #{options[:behavior].to_s.upcase}\n" if options[:behavior]
|
270
|
+
sql << " #{options[:on_null_input].to_s.upcase}\n" if options[:on_null_input]
|
271
|
+
sql << " COST #{options[:cost].to_i}\n" if options[:cost]
|
272
|
+
sql << " ROWS #{options[:rows].to_i}\n" if options[:rows]
|
273
|
+
sql << " " << (set_options(options[:set]) * "\n ") if options[:set]
|
274
|
+
sql.strip
|
275
|
+
end
|
276
|
+
alias :to_s :to_sql
|
277
|
+
end
|
278
|
+
|
279
|
+
# Alters a function. You'll generally want to be calling the
|
280
|
+
# PostgreSQLAdapter#alter_function method rather than risk messing
|
281
|
+
# with this class directly. It's a finicky, delicate flower.
|
282
|
+
class PostgreSQLFunctionAlterer < PostgreSQLFunction
|
283
|
+
def initialize(base, name, args, options = {}) #:nodoc:
|
284
|
+
super(base, name, args, options)
|
285
|
+
|
286
|
+
@sql = options.collect { |k, v| build_statement(k, v) }
|
287
|
+
end
|
288
|
+
|
289
|
+
def empty? #:nodoc:
|
290
|
+
@sql.empty?
|
291
|
+
end
|
292
|
+
|
293
|
+
def to_sql #:nodoc:
|
294
|
+
@sql.join(";\n")
|
295
|
+
end
|
296
|
+
alias :to_s :to_sql
|
297
|
+
|
298
|
+
[ :rename_to, :owner_to, :set_schema, :behavior,
|
299
|
+
:on_null_input, :security, :cost, :rows, :set,
|
300
|
+
:reset
|
301
|
+
].each do |f|
|
302
|
+
self.class_eval <<-EOF
|
303
|
+
def #{f}(v)
|
304
|
+
@sql << build_statement(:#{f}, v)
|
305
|
+
end
|
306
|
+
EOF
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
def build_statement(k, v) #:nodoc:
|
311
|
+
sql = "ALTER FUNCTION #{base.quote_function(@new_name || name)}(#{args}) "
|
312
|
+
sql << case k
|
313
|
+
when :rename_to
|
314
|
+
"RENAME TO #{base.quote_generic_ignore_schema(v)}".tap { @new_name = v }
|
315
|
+
when :owner_to
|
316
|
+
"OWNER TO #{base.quote_role(v)}"
|
317
|
+
when :set_schema
|
318
|
+
"SET SCHEMA #{base.quote_schema(v)}"
|
319
|
+
when :behavior
|
320
|
+
assert_valid_behavior(v)
|
321
|
+
v.to_s.upcase
|
322
|
+
when :on_null_input
|
323
|
+
assert_valid_on_null_input(v)
|
324
|
+
v.to_s.upcase
|
325
|
+
when :security
|
326
|
+
assert_valid_security(v)
|
327
|
+
"SECURITY #{v.to_s.upcase}"
|
328
|
+
when :cost
|
329
|
+
"COST #{v.to_i}"
|
330
|
+
when :rows
|
331
|
+
"ROWS #{v.to_i}"
|
332
|
+
when :set
|
333
|
+
set_options(v) * "\n"
|
334
|
+
when :reset
|
335
|
+
if v.is_a?(Array)
|
336
|
+
v.collect { |vv| "RESET #{base.quote_generic(vv)}" }.join(" ")
|
337
|
+
elsif v == :all
|
338
|
+
'RESET ALL'
|
339
|
+
end
|
340
|
+
end
|
341
|
+
sql
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class InvalidGeometryType < ActiveRecordError #:nodoc:
|
6
|
+
def initialize(type)
|
7
|
+
super("Invalid PostGIS geometry type - #{type}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidGeometryDimensions < ActiveRecordError #:nodoc:
|
12
|
+
end
|
13
|
+
|
14
|
+
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' }
|
19
|
+
})
|
20
|
+
end
|
21
|
+
alias_method_chain :native_database_types, :geometry
|
22
|
+
end
|
23
|
+
|
24
|
+
class PostgreSQLTableDefinition < TableDefinition
|
25
|
+
attr_reader :geometry_columns
|
26
|
+
|
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.
|
30
|
+
#
|
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.
|
35
|
+
#
|
36
|
+
# ==== Options
|
37
|
+
#
|
38
|
+
# * <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.
|
44
|
+
# * <tt>:add_constraints</tt> - automatically creates the CHECK
|
45
|
+
# constraints used to enforce ndims, srid and geometry type.
|
46
|
+
# The default is true.
|
47
|
+
# * <tt>:add_geometry_columns_entry</tt> - automatically adds
|
48
|
+
# an entry to the <tt>geometry_columns</tt> table. We will
|
49
|
+
# try to delete any existing match in <tt>geometry_columns</tt>
|
50
|
+
# before inserting. The default is true.
|
51
|
+
# * <tt>:create_gist_index</tt> - automatically creates a GiST
|
52
|
+
# index for the new geometry column. This option accepts either
|
53
|
+
# a true/false expression or a String. If the value is a String,
|
54
|
+
# we'll use it as the index name. The default is true.
|
55
|
+
# * <tt>:ndims</tt> - the number of dimensions to allow in the
|
56
|
+
# geometry. This value is either 2 or 3 by default depending on
|
57
|
+
# the value of the <tt>:geometry_type</tt> option. If the
|
58
|
+
# <tt>:geometry_type</tt> ends in an "m" (for "measured
|
59
|
+
# geometries" the default is 3); for everything else, it is 2.
|
60
|
+
# * <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 = {})
|
64
|
+
opts = {
|
65
|
+
:geometry_type => :geometry,
|
66
|
+
:add_constraints => true,
|
67
|
+
:add_geometry_columns_entry => true,
|
68
|
+
:create_gist_index => true,
|
69
|
+
:srid => -1
|
70
|
+
}.merge(opts)
|
71
|
+
|
72
|
+
if opts[:ndims].blank?
|
73
|
+
opts[:ndims] = if opts[:geometry_type].to_s.upcase =~ /M$/
|
74
|
+
3
|
75
|
+
else
|
76
|
+
2
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
assert_valid_geometry_type(opts[:geometry_type])
|
81
|
+
assert_valid_ndims(opts[:ndims], opts[:geometry_type])
|
82
|
+
|
83
|
+
column = self[column_name] || ColumnDefinition.new(base, column_name, :geometry)
|
84
|
+
column.default = opts[:default]
|
85
|
+
column.null = opts[:default]
|
86
|
+
|
87
|
+
unless @columns.include?(column)
|
88
|
+
@columns << column
|
89
|
+
if opts[:add_constraints]
|
90
|
+
@table_constraints << PostgreSQLCheckConstraint.new(
|
91
|
+
base,
|
92
|
+
"srid(#{base.quote_column_name(column_name)}) = (#{opts[:srid].to_i})",
|
93
|
+
:name => "enforce_srid_#{column_name}"
|
94
|
+
)
|
95
|
+
|
96
|
+
@table_constraints << PostgreSQLCheckConstraint.new(
|
97
|
+
base,
|
98
|
+
"ndims(#{base.quote_column_name(column_name)}) = #{opts[:ndims].to_i}",
|
99
|
+
:name => "enforce_dims_#{column_name}"
|
100
|
+
)
|
101
|
+
|
102
|
+
if opts[:geometry_type].to_s.upcase != 'GEOMETRY'
|
103
|
+
@table_constraints << PostgreSQLCheckConstraint.new(
|
104
|
+
base,
|
105
|
+
"geometrytype(#{base.quote_column_name(column_name)}) = '#{opts[:geometry_type].to_s.upcase}'::text OR #{base.quote_column_name(column_name)} IS NULL",
|
106
|
+
:name => "enforce_geotype_#{column_name}"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# We want to split up the schema and the table name for the
|
113
|
+
# upcoming geometry_columns rows and GiST index.
|
114
|
+
current_schema, current_table_name = if self.table_name.is_a?(Hash)
|
115
|
+
[ self.table_name.keys.first, self.table_name.values.first ]
|
116
|
+
elsif base.current_schema
|
117
|
+
[ base.current_schema, self.table_name ]
|
118
|
+
else
|
119
|
+
schema, table_name = base.extract_schema_and_table_names(self.table_name)
|
120
|
+
[ schema || 'public', table_name ]
|
121
|
+
end
|
122
|
+
|
123
|
+
@post_processing ||= Array.new
|
124
|
+
|
125
|
+
if opts[:add_geometry_columns_entry]
|
126
|
+
@post_processing << sprintf(
|
127
|
+
"DELETE FROM \"geometry_columns\" WHERE f_table_catalog = '' AND " +
|
128
|
+
"f_table_schema = %s AND " +
|
129
|
+
"f_table_name = %s AND " +
|
130
|
+
"f_geometry_column = %s",
|
131
|
+
base.quote(current_schema.to_s),
|
132
|
+
base.quote(current_table_name.to_s),
|
133
|
+
base.quote(column_name.to_s)
|
134
|
+
)
|
135
|
+
|
136
|
+
@post_processing << sprintf(
|
137
|
+
"INSERT INTO \"geometry_columns\" VALUES ('', %s, %s, %s, %d, %d, %s)",
|
138
|
+
base.quote(current_schema.to_s),
|
139
|
+
base.quote(current_table_name.to_s),
|
140
|
+
base.quote(column_name.to_s),
|
141
|
+
opts[:ndims].to_i,
|
142
|
+
opts[:srid].to_i,
|
143
|
+
base.quote(opts[:geometry_type].to_s)
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
if opts[:create_gist_index]
|
148
|
+
index_name = if opts[:create_gist_index].is_a?(String)
|
149
|
+
opts[:create_gist_index]
|
150
|
+
else
|
151
|
+
"#{current_table_name}_#{column_name}_gist_index"
|
152
|
+
end
|
153
|
+
|
154
|
+
@post_processing << PostgreSQLIndexDefinition.new(
|
155
|
+
base,
|
156
|
+
index_name,
|
157
|
+
current_table_name,
|
158
|
+
column_name,
|
159
|
+
:using => :gist
|
160
|
+
).to_s
|
161
|
+
end
|
162
|
+
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
GEOMETRY_TYPES = [
|
168
|
+
'GEOMETRY',
|
169
|
+
'GEOMETRYCOLLECTION',
|
170
|
+
'POINT',
|
171
|
+
'MULTIPOINT',
|
172
|
+
'POLYGON',
|
173
|
+
'MULTIPOLYGON',
|
174
|
+
'LINESTRING',
|
175
|
+
'MULTILINESTRING',
|
176
|
+
'GEOMETRYCOLLECTIONM',
|
177
|
+
'POINTM',
|
178
|
+
'MULTIPOINTM',
|
179
|
+
'POLYGONM',
|
180
|
+
'MULTIPOLYGONM',
|
181
|
+
'LINESTRINGM',
|
182
|
+
'MULTILINESTRINGM',
|
183
|
+
'CIRCULARSTRING',
|
184
|
+
'CIRCULARSTRINGM',
|
185
|
+
'COMPOUNDCURVE',
|
186
|
+
'COMPOUNDCURVEM',
|
187
|
+
'CURVEPOLYGON',
|
188
|
+
'CURVEPOLYGONM',
|
189
|
+
'MULTICURVE',
|
190
|
+
'MULTICURVEM',
|
191
|
+
'MULTISURFACE',
|
192
|
+
'MULTISURFACEM'
|
193
|
+
].freeze
|
194
|
+
|
195
|
+
def assert_valid_geometry_type(type)
|
196
|
+
if !GEOMETRY_TYPES.include?(type.to_s.upcase)
|
197
|
+
raise ActiveRecord::InvalidGeometryType.new(type)
|
198
|
+
end unless type.nil?
|
199
|
+
end
|
200
|
+
|
201
|
+
def assert_valid_ndims(ndims, type)
|
202
|
+
if !ndims.blank?
|
203
|
+
if type.to_s.upcase =~ /([A-Z]+M)$/ && ndims != 3
|
204
|
+
raise ActiveRecord::InvalidGeometryDimensions.new("Invalid PostGIS geometry dimensions (#{$1} requires 3 dimensions)")
|
205
|
+
elsif ndims < 0 || ndims > 4
|
206
|
+
ralse ActiveRecord::InvalidGeometryDimensions.new("Invalid PostGIS geometry dimensions (should be between 0 and 4 inclusive) - #{ndims}")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|