rails-pg-procs 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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