Skip to content

Commit 28a4d44

Browse files
committed
Add in initial version
1 parent 639f69f commit 28a4d44

22 files changed

+430
-69
lines changed

.gitignore

-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@
55

66
*.lock
77
*.log
8-
*.sqlite3
9-
!gemfiles/**/*.sqlite3

.travis.yml

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
---
66
sudo: false
77
rvm:
8-
- 2.1.5
8+
- 2.3.1
99
gemfile:
10-
- gemfiles/activerecord-4.2/Gemfile.mysql2
11-
- gemfiles/activerecord-4.2/Gemfile.postgresql
12-
- gemfiles/activerecord-4.2/Gemfile.sqlite3
13-
env: POSTGRESQL_DB_USER=postgres MYSQL_DB_USER=travis
10+
- gemfiles/activerecord-5.2/Gemfile.postgresql
11+
env: POSTGRESQL_DB_USER=postgres
1412
addons:
1513
postgresql: '9.4'
1614
before_script: bundle exec rake create_databases

Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
source "http://rubygems.org"
24

35
gemspec

README.md

+59-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# SchemaPlus::Functions
77

8-
TODO: Write a gem description
8+
SchemaPlus::Functions adds support for SQL functions in ActiveRecord.
99

1010
SchemaPlus::Functions is part of the [SchemaPlus](https://github.com/SchemaPlus/) family of Ruby on Rails ActiveRecord extension gems.
1111

@@ -22,20 +22,73 @@ gem.add_dependency "schema_plus_functions" # in a .gemspec
2222

2323
<!-- SCHEMA_DEV: TEMPLATE INSTALLATION - end -->
2424

25+
## Usage
26+
27+
### Migrations
28+
29+
To declare a function use `create_function`:
30+
31+
create_function :test, "start date, stop date DEFAULT NULL::date", <<-END
32+
RETURNS integer
33+
LANGUAGE plpgsql
34+
AS $$
35+
DECLARE
36+
processed INTEGER = 0;
37+
BEGIN
38+
processed = processed + 1;
39+
RETURN processed;
40+
END;
41+
$$
42+
END
43+
44+
To create an aggregate function specify the `function_type`:
45+
46+
create_function :array_cat_agg, "anyarray", <<-END, function_type: :aggregate
47+
(SFUNC=array_cat,STYPE=anyarray)
48+
END
49+
50+
To update a function in a new migration specify the `allow_replace` property
51+
52+
create_function :test, "start date, stop date DEFAULT NULL::date", <<-END, allow_replace: true
53+
RETURNS integer
54+
LANGUAGE plpgsql
55+
AS $$
56+
DECLARE
57+
processed INTEGER = 0;
58+
BEGIN
59+
processed = processed + 5;
60+
RETURN processed;
61+
END;
62+
$$
63+
END
64+
65+
To remove a function use `drop_function` with the name and argument types:
66+
67+
drop_function :test, "start date, stop date"
68+
69+
To remove an aggregate use `drop_function` with the name and argument types with the `function_type`:
70+
71+
drop_function :array_cat_agg, "anyarray", function_type: :aggrgate
72+
73+
### Introspection
74+
75+
You can query the list of user functions at the connection level (uncached):
76+
77+
connection.functions
78+
79+
This will return a array of arrays. The inner array containing the function_name, argument types
80+
and possible function type.
81+
2582
## Compatibility
2683

2784
SchemaPlus::Functions is tested on:
2885

2986
<!-- SCHEMA_DEV: MATRIX - begin -->
3087
<!-- These lines are auto-generated by schema_dev based on schema_dev.yml -->
31-
* ruby **2.1.5** with activerecord **4.2**, using **mysql2**, **sqlite3** or **postgresql**
88+
* ruby **2.3.1** with activerecord **5.2**, using **postgresql**
3289

3390
<!-- SCHEMA_DEV: MATRIX - end -->
3491

35-
## Usage
36-
37-
TODO: Write usage instructions here
38-
3992
## History
4093

4194
* 0.1.0 - Initial release

Rakefile

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require 'bundler'
24
Bundler::GemHelper.install_tasks
35

gemfiles/activerecord-4.2/Gemfile.mysql2

-10
This file was deleted.

gemfiles/activerecord-4.2/Gemfile.sqlite3

-10
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
eval File.read File.expand_path('../../Gemfile.base', __FILE__)
22

3-
gem "activerecord", "~> 4.2.6"
3+
gem "activerecord", ">= 5.2.0.beta0", "< 5.3"

gemfiles/activerecord-4.2/Gemfile.postgresql gemfiles/activerecord-5.2/Gemfile.postgresql

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ require "pathname"
22
eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
33

44
platform :ruby do
5-
gem "pg", "< 1"
5+
gem "pg"
66
end
77

88
platform :jruby do

lib/schema_plus/functions.rb

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
# frozen_string_literal: true
2+
13
require 'schema_plus/core'
24

35
require_relative 'functions/version'
6+
require_relative 'functions/active_record/connection_adapters/abstract_adapter'
7+
require_relative 'functions/active_record/migration/command_recorder'
8+
require_relative 'functions/middleware'
49

5-
# Load any mixins to ActiveRecord modules, such as:
6-
#
7-
#require_relative 'functions/active_record/base'
8-
9-
# Load any middleware, such as:
10-
#
11-
# require_relative 'functions/middleware/model'
10+
module SchemaPlus::Functions
11+
module ActiveRecord
12+
module ConnectionAdapters
13+
autoload :PostgresqlAdapter, 'schema_plus/functions/active_record/connection_adapters/postgresql_adapter'
14+
end
15+
end
16+
end
1217

13-
SchemaMonkey.register SchemaPlus::Functions
18+
SchemaMonkey.register SchemaPlus::Functions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
module SchemaPlus::Functions
4+
module ActiveRecord
5+
module ConnectionAdapters
6+
module AbstractAdapter
7+
# Create a function. Valid options are :force,
8+
# :allow_replace, and :function_type
9+
def create_function(function_name, params, definition, options = {})
10+
SchemaMonkey::Middleware::Migration::CreateFunction.start(connection: self, function_name: function_name, params: params, definition: definition, options: options) do |env|
11+
function_name = env.function_name
12+
function_type = (options[:function_type] || :function).to_s.upcase
13+
params = env.params
14+
definition = env.definition
15+
options = env.options
16+
17+
definition = definition.to_sql if definition.respond_to? :to_sql
18+
if options[:force]
19+
drop_function(function_name, params, function_type: options[:function_type], if_exists: true)
20+
end
21+
22+
command = if options[:allow_replace]
23+
"CREATE OR REPLACE"
24+
else
25+
"CREATE"
26+
end
27+
execute "#{command} #{function_type} #{function_name}(#{params}) #{definition}"
28+
end
29+
end
30+
31+
# Remove a function. Valid options are :function_type,
32+
# :if_exists, and :cascade
33+
#
34+
# If your function type is an aggregate, you must specify the type
35+
#
36+
# drop_function 'my_func', 'int', if_exists: true, cascade: true
37+
# drop_function 'my_agg', 'int', function_type: :aggregate
38+
def drop_function(function_name, params, options = {})
39+
SchemaMonkey::Middleware::Migration::CreateFunction.start(connection: self, function_name: function_name, params: params, options: options) do |env|
40+
function_name = env.function_name
41+
params = env.params
42+
options = env.options
43+
function_type = (options[:function_type] || :function).to_s.upcase
44+
45+
sql = "DROP #{function_type}"
46+
sql += " IF EXISTS" if options[:if_exists]
47+
sql += " #{function_name}(#{params})"
48+
sql += " CASCADE" if options[:cascade]
49+
50+
execute sql
51+
end
52+
end
53+
54+
#####################################################################
55+
#
56+
# The functions below here are abstract; each subclass should
57+
# define them all. Defining them here only for reference.
58+
#
59+
60+
# (abstract) Return the Function objects for functions
61+
def functions(name = nil)
62+
raise "Internal Error: Connection adapter did not override abstract function"
63+
end
64+
65+
# (abstract) Return the Function definition for the named function and parameter set
66+
def function_definition(function_name, params, name = nil)
67+
raise "Internal Error: Connection adapter did not override abstract function"
68+
end
69+
end
70+
end
71+
end
72+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
module SchemaPlus::Functions
4+
module ActiveRecord
5+
module ConnectionAdapters
6+
module PostgresqlAdapter
7+
def drop_function(function_name, params, options = {})
8+
clean_params = params.gsub(/ DEFAULT[^,]+/i, '')
9+
super(function_name, clean_params, options)
10+
end
11+
12+
def functions(name = nil) #:nodoc:
13+
SchemaMonkey::Middleware::Schema::Functions.start(connection: self, query_name: name, functions: []) do |env|
14+
sql = <<-SQL
15+
SELECT P.proname as function_name, pg_get_function_identity_arguments(P.oid), proisagg as is_agg
16+
FROM pg_proc P
17+
WHERE
18+
pronamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)))
19+
AND NOT EXISTS (SELECT 1 FROM pg_depend WHERE classid = 'pg_proc'::regclass
20+
AND objid = p.oid AND deptype = 'i')
21+
AND NOT EXISTS (SELECT 1 FROM pg_depend WHERE classid = 'pg_proc'::regclass
22+
AND objid = p.oid AND refclassid = 'pg_extension'::regclass AND deptype = 'e')
23+
ORDER BY 1,2
24+
SQL
25+
26+
env.functions += env.connection.query(sql, env.query_name).map do |row|
27+
options = {}
28+
options[:function_type] = :aggregate if row[2]
29+
[row[0], row[1], options]
30+
end
31+
end.functions
32+
end
33+
34+
def function_definition(function_name, params, name = nil) #:nodoc:
35+
data = SchemaMonkey::Middleware::Schema::FunctionDefinition.start(connection: self, function_name: function_name, params: params, query_name: name) do |env|
36+
result = env.connection.query(<<-SQL, env.query_name)
37+
SELECT prosrc,
38+
pg_get_function_arguments(p.oid),
39+
pg_catalog.pg_get_function_result(p.oid) AS funcresult,
40+
(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname,
41+
a.aggtransfn as transfn,
42+
format_type(a.aggtranstype, null) as transtype
43+
FROM pg_proc p
44+
LEFT JOIN pg_aggregate a ON a.aggfnoid = p.oid
45+
WHERE
46+
pronamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)))
47+
AND proname = '#{quote_string(function_name)}'
48+
AND pg_get_function_identity_arguments(P.oid) = '#{quote_string(params)}'
49+
SQL
50+
51+
row = result.first
52+
53+
function_type = nil
54+
55+
unless row.nil?
56+
sql = if row[4].present? || row[0] == 'aggregate_dummy'
57+
# it's an aggregate function
58+
function_type = :aggregate
59+
"(SFUNC=#{row[4]},STYPE=#{row[5]})"
60+
else
61+
"RETURNS #{row[2]} LANGUAGE #{row[3]} AS $$#{row[0]}$$"
62+
end
63+
env.params = row[1]
64+
env.definition = sql
65+
env.function_type = function_type
66+
end
67+
end
68+
69+
return data.params, data.definition, data.function_type
70+
end
71+
end
72+
end
73+
end
74+
end
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module SchemaPlus::Functions
4+
module ActiveRecord
5+
module Migration
6+
module CommandRecorder
7+
def create_function(*args, &block)
8+
record(:create_function, args, &block)
9+
end
10+
11+
def drop_function(*args, &block)
12+
record(:drop_function, args, &block)
13+
end
14+
15+
def invert_create_function(args)
16+
options = {}
17+
options[:function_type] = args[3][:function_type] if args[3].has_key?(:function_type)
18+
[:drop_function, [args.first, args.second, options]]
19+
end
20+
end
21+
end
22+
end
23+
end

0 commit comments

Comments
 (0)