rails-pg-procs 1.0.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 (51) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +20 -0
  4. data/README +15 -0
  5. data/README.rdoc +18 -0
  6. data/Rakefile +60 -0
  7. data/VERSION +1 -0
  8. data/docs/classes/ActiveRecord.html +117 -0
  9. data/docs/classes/ActiveRecord/ConnectionAdapters.html +113 -0
  10. data/docs/classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html +589 -0
  11. data/docs/classes/ActiveRecord/ConnectionAdapters/ProcedureDefinition.html +110 -0
  12. data/docs/classes/ActiveRecord/ConnectionAdapters/TriggerDefinition.html +519 -0
  13. data/docs/classes/ActiveRecord/ConnectionAdapters/TypeDefinition.html +110 -0
  14. data/docs/classes/ActiveRecord/SchemaDumper.html +371 -0
  15. data/docs/classes/Inflector.html +164 -0
  16. data/docs/classes/SchemaProcs.html +211 -0
  17. data/docs/classes/SqlFormat.html +139 -0
  18. data/docs/classes/String.html +117 -0
  19. data/docs/classes/Symbol.html +117 -0
  20. data/docs/created.rid +1 -0
  21. data/docs/fr_class_index.html +36 -0
  22. data/docs/fr_method_index.html +63 -0
  23. data/docs/index.html +22 -0
  24. data/docs/rdoc-style.css +208 -0
  25. data/init.rb +3 -0
  26. data/install.rb +1 -0
  27. data/lib/connection_adapters/aggregagtes_definition.rb +1 -0
  28. data/lib/connection_adapters/connection_adapters.rb +8 -0
  29. data/lib/connection_adapters/index_definition.rb +24 -0
  30. data/lib/connection_adapters/postgresql_adapter.rb +256 -0
  31. data/lib/connection_adapters/procedure_definition.rb +6 -0
  32. data/lib/connection_adapters/schema_definition.rb +24 -0
  33. data/lib/connection_adapters/schema_statements.rb +11 -0
  34. data/lib/connection_adapters/trigger_definition.rb +114 -0
  35. data/lib/connection_adapters/type_definition.rb +17 -0
  36. data/lib/connection_adapters/view_definition.rb +34 -0
  37. data/lib/inflector.rb +10 -0
  38. data/lib/rails_pg_procs.rb +8 -0
  39. data/lib/schema_dumper.rb +96 -0
  40. data/lib/schema_procs.rb +19 -0
  41. data/lib/sql_format.rb +17 -0
  42. data/tasks/rails_pg_procs.rake +4 -0
  43. data/test/connection.rb +16 -0
  44. data/test/procedure_test.rb +78 -0
  45. data/test/rails_pg_procs_test.rb +246 -0
  46. data/test/test_helper.rb +41 -0
  47. data/test/trigger_test.rb +87 -0
  48. data/test/type_test.rb +46 -0
  49. data/test/view_test.rb +36 -0
  50. data/uninstall.rb +1 -0
  51. metadata +112 -0
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Include hook code here
2
+ $LOAD_PATH.push File.dirname(__FILE__)
3
+ require 'lib/rails_pg_procs'
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1 @@
1
+ # TODO -- Add Aggregates ability
@@ -0,0 +1,8 @@
1
+ require 'connection_adapters/schema_statements'
2
+ require 'connection_adapters/schema_definition'
3
+ require 'connection_adapters/aggregagtes_definition'
4
+ require 'connection_adapters/postgresql_adapter'
5
+ require 'connection_adapters/procedure_definition'
6
+ require 'connection_adapters/trigger_definition'
7
+ require 'connection_adapters/type_definition'
8
+ require 'connection_adapters/view_definition'
@@ -0,0 +1,24 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns)
4
+ include SchemaProcs
5
+
6
+ def to_rdl add=true, options={}
7
+ return " add_index #{table.inspect}, #{columns.inspect}, :name => #{name.inspect}#{', :unique => true' if unique || unique == 't'}" if add
8
+ # " remove_index #{table.inspect}, #{columns.inspect}, :name => #{name.inspect}#{', :unique => true' if unique || unique == 't'}" if add
9
+ end
10
+
11
+ def to_sql(action="create", options={})
12
+ case action
13
+ when "create", :create
14
+ "CREATE INDEX #{name.to_sql_name}"
15
+ # TODO - [ schema_element ]
16
+ when "drop", :drop
17
+ "DROP INDEX #{quote_column_name(index_name(table, options))} ON #{table}"
18
+ # "DROP SCHEMA #{name.to_sql_name} #{cascade_or_restrict(options[:cascade])}"
19
+ # TODO - [ IF EXISTS ]
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,256 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # TODO -- Add Aggregates ability
4
+ class PostgreSQLAdapter < AbstractAdapter
5
+ include SchemaProcs
6
+
7
+ @@ignore_namespaces = %w(pg_toast pg_temp_1 pg_catalog public information_schema)
8
+
9
+ def drop_database name
10
+ execute "DROP DATABASE#{' IF EXISTS' if postgresql_version >= 80200} #{name}"
11
+ end
12
+
13
+ def schemas
14
+ query(<<-end_sql).collect {|row| SchemaDefinition.new(*row) }
15
+ SELECT N.nspname, S.usename
16
+ FROM pg_namespace N
17
+ JOIN pg_shadow S ON (N.nspowner = S.usesysid)
18
+ WHERE N.nspname NOT IN (#{@@ignore_namespaces.collect {|nsp| nsp.to_sql_value }.join(',')})
19
+ end_sql
20
+ end
21
+
22
+ def procedures(lang=nil)
23
+ query <<-end_sql
24
+ SELECT P.oid, proname, pronamespace, proowner, lanname, proisagg, prosecdef, proisstrict, proretset, provolatile, pronargs, prorettype, proargtypes, proargnames, prosrc, probin, proacl
25
+ FROM pg_proc P
26
+ JOIN pg_language L ON (P.prolang = L.oid)
27
+ JOIN pg_namespace N ON (P.pronamespace = N.oid)
28
+ WHERE N.nspname = 'public'
29
+ AND (proisagg = 'f')
30
+ #{'AND (lanname ' + lang + ')'unless lang.nil?}
31
+ end_sql
32
+ end
33
+
34
+ def triggers(table_name)
35
+ query(<<-end_sql).collect {|row| TriggerDefinition.new(*row) }
36
+ SELECT T.oid, C.relname, T.tgname, T.tgtype, P.proname
37
+ FROM pg_trigger T
38
+ JOIN pg_class C ON (T.tgrelid = C.OID AND C.relname = '#{table_name}' AND T.tgisconstraint = 'f')
39
+ JOIN pg_proc P ON (T.tgfoid = P.OID)
40
+ end_sql
41
+ end
42
+
43
+ def types
44
+ result = query(<<-end_sql)
45
+ SELECT T.oid, T.typname, A.attname, format_type(A.atttypid, A.atttypmod) AS type
46
+ FROM pg_type T
47
+ JOIN pg_class C ON (T.typrelid = C.oid)
48
+ JOIN pg_attribute A ON (A.attrelid = C.oid AND C.relkind = 'c')
49
+ end_sql
50
+
51
+ type_id = nil
52
+ types = []
53
+ result.each { |row|
54
+ if type_id != row[0]
55
+ types << TypeDefinition.new(row[0], row[1], [])
56
+ type_id = row[0]
57
+ end
58
+
59
+ types.last.columns << [row[2], row[3]]
60
+ }
61
+
62
+ types
63
+ end
64
+
65
+ # def tables(name = nil)
66
+ # schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
67
+ # query(<<-SQL, name).map { |row| row[0] << '.' << row[1] }
68
+ # SELECT N.nspname, C.relname
69
+ # FROM pg_class C
70
+ # JOIN pg_namespace N ON (C.relnamespace = N.oid)
71
+ # WHERE N.nspname IN (#{schemas})
72
+ # AND C.relkind = 'r'
73
+ # SQL
74
+ # end
75
+
76
+ # TODO Implement this
77
+ def views #:nodoc:
78
+ end
79
+
80
+ def columns(table_name, name = nil)
81
+ # Limit, precision, and scale are all handled by the superclass.
82
+ column_definitions(table_name).collect do |name, type, default, notnull|
83
+ PostgreSQLColumn.new(name, default, type, notnull == 'f')
84
+ end
85
+ end
86
+
87
+ def create_type(name, *columns)
88
+ if type = types.find {|typ| typ.name == name.to_s }
89
+ drop_type(type.name, true)
90
+ end
91
+ execute get_type_query(name, *columns)
92
+ end
93
+
94
+ def drop_type(name, cascade=false)
95
+ # puts "drop_type(#{name.to_sql_name})"
96
+ execute "DROP TYPE #{name.to_sql_name} #{cascade_or_restrict(cascade)}"
97
+ end
98
+
99
+ def create_view(name, columns=[], options={}, &block)
100
+ view = ViewDefinition.new(0, name, columns) { yield }
101
+ execute view.to_sql(:create, options)
102
+ end
103
+
104
+ def drop_view(name, options={})
105
+ view = ViewDefinition.new(0, name)
106
+ execute view.to_sql(:drop, options)
107
+ end
108
+
109
+ def create_schema(name, owner='postgres', options={})
110
+ if schema = schemas.find {|schema| schema.name.to_s == name.to_s }
111
+ drop_schema(schema.name, :cascade => true)
112
+ end
113
+ execute (schema = SchemaDefinition.new(name, owner)).to_sql(:create, options)
114
+ self.schema_search_path = (self.schema_search_path.split(",") | [schema.name]).join(',')
115
+ # self.schema_search_path = ( [schema.name] | self.schema_search_path.split(",") ).join(',')
116
+ end
117
+
118
+ def drop_schema(name, options={})
119
+ search_path = self.schema_search_path.split(",")
120
+ self.schema_search_path = search_path.join(',') if search_path.delete(name.to_s)
121
+ if schema = schemas.find {|schema| schema.name.to_s == name.to_s }
122
+ execute SchemaDefinition.new(name).to_sql(:drop, options)
123
+ end
124
+ end
125
+
126
+ # Add a trigger to a table
127
+ def add_trigger(table, events, options={})
128
+ events += [:row] if options.delete(:row)
129
+ events += [:before] if options.delete(:before)
130
+ trigger = TriggerDefinition.new(0, table, options[:name], events, options[:function])
131
+ execute trigger.to_sql_create
132
+ end
133
+
134
+ # DROP TRIGGER name ON table [ CASCADE | RESTRICT ]
135
+ def remove_trigger(table, name, options={})
136
+ options[:name] = name
137
+ execute "DROP TRIGGER #{trigger_name(table, [], options).to_sql_name} ON #{table} #{cascade_or_restrict(options[:deep])};"
138
+ end
139
+
140
+ # Create a stored procedure
141
+ def create_proc(name, columns=[], options={}, &block)
142
+ if select_value("SELECT count(oid) FROM pg_language WHERE lanname = 'plpgsql' ","count").to_i == 0
143
+ execute("CREATE FUNCTION plpgsql_call_handler() RETURNS language_handler AS '$libdir/plpgsql', 'plpgsql_call_handler' LANGUAGE c")
144
+ execute("CREATE TRUSTED PROCEDURAL LANGUAGE plpgsql HANDLER plpgsql_call_handler")
145
+ end
146
+
147
+ if options[:force]
148
+ drop_proc(name, columns) rescue nil
149
+ end
150
+
151
+ if block_given?
152
+ execute get_proc_query(name, columns, options) { yield }
153
+ elsif options[:resource]
154
+ execute get_proc_query(name, columns, options)
155
+ else
156
+ raise StatementInvalid.new("Missing function source")
157
+ end
158
+ end
159
+
160
+ # DROP FUNCTION name ( [ type [, ...] ] ) [ CASCADE | RESTRICT ]
161
+ # default RESTRICT
162
+ def drop_proc(name, columns=[], cascade=false)
163
+ execute "DROP FUNCTION #{name.to_sql_name}(#{columns.collect {|column| column}.join(", ")}) #{cascade_or_restrict(cascade)};"
164
+ end
165
+
166
+ private
167
+ # def column_definitions(table_name) #:nodoc:
168
+ # schema, table_name = table_name.split '.'
169
+ # unless table_name
170
+ # table_name = schema
171
+ # schema = 'public'
172
+ # end
173
+ # query <<-end_sql
174
+ # SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
175
+ # FROM pg_attribute a LEFT JOIN pg_attrdef d
176
+ # ON a.attrelid = d.adrelid AND a.attnum = d.adnum
177
+ # JOIN pg_class c ON (a.attrelid = c.oid)
178
+ # JOIN pg_namespace n ON (c.relnamespace = n.oid)
179
+ # WHERE c.relname = '#{table_name}'
180
+ # AND n.nspname = '#{schema}'
181
+ # AND a.attnum > 0 AND NOT a.attisdropped
182
+ # ORDER BY a.attnum
183
+ # end_sql
184
+ # end
185
+
186
+ def trigger_name(table, events=[], options={})
187
+ options[:name] || Inflector.triggerize(table, events, options[:before])
188
+ end
189
+
190
+ # Helper function that builds the sql query used to create a stored procedure.
191
+ # Mostly this is here so we can unit test the generated sql.
192
+ # Either an option[:resource] or block must be defined for this method.
193
+ # Otherwise an ActiveRecord::StatementInvalid exception is raised.
194
+ # Defaults are:
195
+ # RETURNS (no default -- which is cheap since that means you have to call this method w/ the options Hash) TODO: fix this
196
+ # LANGUAGE = plpgsql (The plugin will add this if you don't have it added already)
197
+ # behavior = VOLATILE (Don't specify IMMUTABLE or STABLE and this will be added for you)
198
+ # strict = CALLED ON NULL INPUT (Otherwise STRICT, According to the 8.0 manual STRICT and RETURNS NULL ON NULL INPUT (RNONI)
199
+ # behave the same so I didn't make a case for RNONI)
200
+ # user = INVOKER
201
+ def delim(name, options)
202
+ name = name.split('.').last if name.is_a?(String) && name.include?('.')
203
+ options[:delim] || "$#{ActiveSupport::Inflector.underscore(name)}_body$"
204
+ end
205
+
206
+ # From PostgreSQL
207
+ ## CREATE [ OR REPLACE ] FUNCTION
208
+ ## name ( [ [ argmode ] [ argname ] argtype [, ...] ] )
209
+ ## [ RETURNS rettype ]
210
+ ## { LANGUAGE langname
211
+ ## | IMMUTABLE | STABLE | VOLATILE
212
+ ## | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
213
+ # | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
214
+ ## | AS 'definition'
215
+ # | AS 'obj_file', 'link_symbol'
216
+ # } ...
217
+ # [ WITH ( isStrict &| isCacheable ) ]
218
+ # TODO Implement [ [ argmode ] [ argname ] argtype ]
219
+ def get_proc_query(name, columns=[], options={}, &block)
220
+ returns = "RETURNS#{' SETOF' if options[:set]} #{options[:return] || 'VOID'}"
221
+ lang = options[:lang] || "plpgsql"
222
+
223
+ if block_given?
224
+ body = "#{delim(name, options)}\n#{yield}\n#{delim(name, options)}"
225
+ elsif options[:resource]
226
+ options[:resource] += [name] if options[:resource].size == 1
227
+ body = options[:resource].collect {|res| "'#{res}'" }.join(", ")
228
+ else
229
+ raise StatementInvalid.new("Missing function source")
230
+ end
231
+
232
+ result = "
233
+ CREATE OR REPLACE FUNCTION #{name.to_sql_name}(#{columns.collect{|column| column}.join(", ")}) #{returns} AS
234
+ #{body}
235
+ LANGUAGE #{lang}
236
+ #{ behavior(options[:behavior] || 'v').upcase }
237
+ #{ strict_or_null(options[:strict]) }
238
+ EXTERNAL SECURITY #{ definer_or_invoker(options[:definer]) }
239
+ "
240
+ end
241
+
242
+ def get_type_query(name, *columns)
243
+ raise StatementInvalid.new if columns.empty?
244
+ "CREATE TYPE #{quote_column_name(name)} AS (
245
+ #{columns.collect{|column,type|
246
+ if column.is_a?(Hash)
247
+ column.collect { |column, type| "#{quote_column_name(column)} #{type}" }
248
+ else
249
+ "#{quote_column_name(column)} #{type}"
250
+ end
251
+ }.join(",\n")}
252
+ )"
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class ProcedureDefinition < Struct.new(:id, :name)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class SchemaDefinition < Struct.new(:name, :owner)
4
+ include SchemaProcs
5
+
6
+ def to_rdl
7
+ " create_schema #{name.to_sql_name}, #{owner.to_sql_name}"
8
+ end
9
+
10
+ # CREATE SCHEMA schemaname [ AUTHORIZATION username ] [ schema_element [ ... ] ]
11
+ # DROP SCHEMA [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ]
12
+ def to_sql(action="create", options={})
13
+ case action
14
+ when "create", :create
15
+ "CREATE SCHEMA #{name.to_sql_name} AUTHORIZATION #{owner.to_sql_name}"
16
+ # TODO - [ schema_element ]
17
+ when "drop", :drop
18
+ "DROP SCHEMA #{name.to_sql_name} #{cascade_or_restrict(options[:cascade])}"
19
+ # TODO - [ IF EXISTS ]
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters # :nodoc:
3
+ module SchemaStatements
4
+ include SchemaProcs
5
+ def drop_table name, options={}
6
+ execute "DROP TABLE #{name.inspect} #{cascade_or_restrict(options[:cascade])}"
7
+ end
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,114 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class TriggerDefinition
4
+ CLEAN = 0b0
5
+ ROW = 0b00001
6
+ BEFORE = 0b00010
7
+ INSERT = 0b00100
8
+ DELETE = 0b01000
9
+ UPDATE = 0b10000
10
+
11
+ attr_accessor :id, :table, :name, :procedure_name
12
+ attr_reader :binary_type
13
+ def initialize(id, table, name=nil, binary_type=[], procedure_name=nil)
14
+ @id = id
15
+ @table = table
16
+ self.binary_type = binary_type
17
+ self.name = (name || triggerized_name)
18
+ self.procedure_name = (procedure_name || name || triggerized_name)
19
+ end
20
+
21
+ # that's to_r(uby)d(efinition)l(anguage)
22
+ def to_rdl()
23
+ " add_trigger #{table.to_sql_name}" <<
24
+ ", [" + events.join(", ") + "]" <<
25
+ ( before? ? ", :before => true" : "") <<
26
+ ( row? ? ", :row => true" : "") <<
27
+ (!triggerized? ? ", :name => #{ActiveSupport::Inflector.symbolize(name)}" : "") <<
28
+ (!triggerized?(procedure_name) ? ", :function => #{ActiveSupport::Inflector.symbolize(procedure_name)}" : "")
29
+ end
30
+
31
+ def binary_type=(*types)
32
+ case types[0]
33
+ when Fixnum, Array
34
+ @binary_type = bin_typ(types[0])
35
+ else
36
+ @binary_type = bin_typ(types)
37
+ end
38
+ end
39
+
40
+ # CREATE TRIGGER name { BEFORE | AFTER } { event [ OR ... ] }
41
+ # ON table [ FOR [ EACH ] { ROW | STATEMENT } ]
42
+ # EXECUTE PROCEDURE funcname ( arguments )
43
+ def to_sql_create()
44
+ result = "CREATE TRIGGER " <<
45
+ name.to_sql_name <<
46
+ (before? ? " BEFORE" : " AFTER") <<
47
+ " " <<
48
+ (
49
+ events.collect {|event|
50
+ event.to_s.upcase.gsub(/^:/, '') }.join(" OR ")
51
+ ) <<
52
+ " ON " <<
53
+ table.to_sql_name <<
54
+ " FOR EACH " <<
55
+ (row? ? "ROW" : "STATEMENT") <<
56
+ " EXECUTE PROCEDURE " <<
57
+ procedure_name.to_sql_name <<
58
+ "();"
59
+ result
60
+ end
61
+
62
+ def triggerized?(nam=nil)
63
+ nam ||= self.name
64
+ triggerized_name == nam
65
+ end
66
+
67
+ def before?
68
+ calc(BEFORE)
69
+ end
70
+
71
+ def row?
72
+ calc(ROW)
73
+ end
74
+
75
+ private
76
+
77
+ def triggerized_name
78
+ ActiveSupport::Inflector.triggerize(table, events, calc(BEFORE))
79
+ end
80
+
81
+ def events
82
+ events = []
83
+ events.push(":insert") if calc(INSERT)
84
+ events.push(":update") if calc(UPDATE)
85
+ events.push(":delete") if calc(DELETE)
86
+ events
87
+ end
88
+
89
+ def calc(bin)
90
+ eval(sprintf("0b%0.8b", self.binary_type())) & bin > 0
91
+ end
92
+
93
+ def bin_typ(typs)
94
+ case typs
95
+ when Fixnum
96
+ return typs
97
+ when Symbol
98
+ return bin_typ(typs.to_s)
99
+ when String
100
+ return typs.to_i if typs =~ /^\d+$/
101
+ return self.class.const_get(typs.upcase.to_sym)
102
+ when Array
103
+ ctype = 0
104
+ typs.each {|typ|
105
+ ctype += bin_typ(typ)
106
+ }
107
+ end
108
+ ctype
109
+ end
110
+
111
+ # end private
112
+ end
113
+ end
114
+ end