dumbo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +9 -0
- data/bin/dumbo +58 -0
- data/config/boot.rb +15 -0
- data/config/database.yml +31 -0
- data/dumbo.gemspec +31 -0
- data/lib/dumbo.rb +21 -0
- data/lib/dumbo/aggregate.rb +57 -0
- data/lib/dumbo/base_type.rb +71 -0
- data/lib/dumbo/cast.rb +49 -0
- data/lib/dumbo/composite_type.rb +31 -0
- data/lib/dumbo/db_task.rb +57 -0
- data/lib/dumbo/dependency_resolver.rb +105 -0
- data/lib/dumbo/enum_type.rb +28 -0
- data/lib/dumbo/extension.rb +73 -0
- data/lib/dumbo/extension_migrator.rb +66 -0
- data/lib/dumbo/extension_version.rb +25 -0
- data/lib/dumbo/function.rb +101 -0
- data/lib/dumbo/operator.rb +74 -0
- data/lib/dumbo/pg_object.rb +80 -0
- data/lib/dumbo/rake_task.rb +121 -0
- data/lib/dumbo/range_type.rb +43 -0
- data/lib/dumbo/type.rb +31 -0
- data/lib/dumbo/version.rb +3 -0
- data/lib/tasks/db.rake +52 -0
- data/lib/tasks/dumbo.rake +23 -0
- data/spec/Makefile +6 -0
- data/spec/aggregate_spec.rb +41 -0
- data/spec/cast_spec.rb +20 -0
- data/spec/dumbo_sample--0.0.1.sql +5 -0
- data/spec/dumbo_sample--0.0.2.sql +5 -0
- data/spec/dumbo_sample.control +5 -0
- data/spec/extension_migrator_spec.rb +40 -0
- data/spec/extension_spec.rb +19 -0
- data/spec/operator_spec.rb +42 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/sql_helper.rb +23 -0
- data/spec/type_spec.rb +95 -0
- data/template/Gemfile +3 -0
- data/template/Makefile.erb +6 -0
- data/template/Rakefile +15 -0
- data/template/config/database.yml.erb +31 -0
- data/template/spec/sample_spec.rb.erb +13 -0
- data/template/sql/sample.sql +5 -0
- metadata +230 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class CompositeType < Type
|
3
|
+
attr_accessor :attributes
|
4
|
+
|
5
|
+
def load_attributes
|
6
|
+
super
|
7
|
+
res = execute <<-SQL
|
8
|
+
SELECT
|
9
|
+
attname,
|
10
|
+
format_type(t.oid,NULL) AS typname
|
11
|
+
FROM pg_attribute att
|
12
|
+
JOIN pg_type t ON t.oid=atttypid
|
13
|
+
|
14
|
+
WHERE att.attrelid=#{typrelid}
|
15
|
+
ORDER by attnum
|
16
|
+
SQL
|
17
|
+
|
18
|
+
attribute = Struct.new(:name, :type)
|
19
|
+
@attributes = res.map{|r| attribute.new(r['attname'],r['typname'])}
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_sql
|
23
|
+
attr_str = attributes.map{|a| "#{a.name} #{a.type}"}.join(",\n ")
|
24
|
+
<<-SQL.gsub(/^ {6}/, '')
|
25
|
+
CREATE TYPE #{name} AS (
|
26
|
+
#{attr_str}
|
27
|
+
);
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "rake"
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module Dumbo
|
6
|
+
class DbTask < ::Rake::TaskLib
|
7
|
+
attr_accessor :name
|
8
|
+
def initialize(name = 'db')
|
9
|
+
@name = name
|
10
|
+
|
11
|
+
namespace name do
|
12
|
+
task environment: ['db:configure_connection' ]
|
13
|
+
|
14
|
+
task :configuration do
|
15
|
+
@config = YAML.load_file('config/database.yml')[ENV['DUMBO_ENV']]
|
16
|
+
end
|
17
|
+
|
18
|
+
task configure_connection: :configuration do
|
19
|
+
ActiveRecord::Base.establish_connection @config
|
20
|
+
ActiveRecord::Base.logger = Logger.new STDOUT if @config['logger']
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Create the database from config/database.yml for the current ENV'
|
24
|
+
task create: :environment do
|
25
|
+
create_database @config
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Drops the database for the current ENV'
|
29
|
+
task drop: :environment do
|
30
|
+
ActiveRecord::Base.establish_connection @config.merge('database' => nil)
|
31
|
+
ActiveRecord::Base.connection.drop_database @config['database']
|
32
|
+
end
|
33
|
+
|
34
|
+
namespace :test do
|
35
|
+
task :environment do
|
36
|
+
ENV['DUMBO_ENV'] ||= 'test'
|
37
|
+
ActiveRecord::Schema.verbose = false
|
38
|
+
end
|
39
|
+
|
40
|
+
task load_structure: :environment do
|
41
|
+
filename = ENV['DB_STRUCTURE'] || File.join("db", "structure.sql")
|
42
|
+
ActiveRecord::Tasks::DatabaseTasks.structure_load(@config, filename)
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Re-create and prepare test database"
|
46
|
+
task prepare: [:environment, :drop, :create]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_database(config)
|
52
|
+
ActiveRecord::Base.establish_connection config.merge('database' => nil)
|
53
|
+
ActiveRecord::Base.connection.create_database config['database'], config
|
54
|
+
ActiveRecord::Base.establish_connection config
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
3
|
+
|
4
|
+
module Dumbo
|
5
|
+
|
6
|
+
class DependencyNotFound < StandardError
|
7
|
+
attr_accessor :dep, :file
|
8
|
+
|
9
|
+
def initialize(dep, file)
|
10
|
+
super "Can't find dependency #{dep} for file: #{file}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DependencyResolver
|
15
|
+
def self.depends_pattern
|
16
|
+
/\s*--\s*require +([^\s'";]+)/
|
17
|
+
end
|
18
|
+
|
19
|
+
# Constructor
|
20
|
+
# accepts an array of files
|
21
|
+
def initialize(file_list)
|
22
|
+
@file_list = file_list
|
23
|
+
end
|
24
|
+
|
25
|
+
def resolve
|
26
|
+
list = dependency_list.sort{|a,b| a.last.size <=> b.last.size}
|
27
|
+
resolve_list(list)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def resolve_list(list)
|
33
|
+
@resolve_list = []
|
34
|
+
@temp_list = list
|
35
|
+
loops = 0
|
36
|
+
until @temp_list.empty? || loops > 10 do
|
37
|
+
@temp_list.each do |(file, deps)|
|
38
|
+
_resolve(file, deps)
|
39
|
+
end
|
40
|
+
loops +=1
|
41
|
+
end
|
42
|
+
|
43
|
+
raise "Can't resolve dependencies" if loops > 10
|
44
|
+
|
45
|
+
@resolve_list
|
46
|
+
end
|
47
|
+
|
48
|
+
def _resolve(file, deps)
|
49
|
+
if deps.empty?
|
50
|
+
@resolve_list.push(@temp_list.shift.first)
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
left = deps - @resolve_list
|
55
|
+
if left.empty?
|
56
|
+
@resolve_list.push(@temp_list.shift.first)
|
57
|
+
return
|
58
|
+
else
|
59
|
+
@temp_list.push @temp_list.shift
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def dependency_list
|
65
|
+
@file_list.map do |file|
|
66
|
+
deps = []
|
67
|
+
IO.foreach(file) do |line|
|
68
|
+
catch(:done) do
|
69
|
+
dep = parse(line)
|
70
|
+
deps << relative_path(dep,file) if dep
|
71
|
+
end
|
72
|
+
end
|
73
|
+
[file, deps]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def encoded_line(line)
|
78
|
+
if String.method_defined?(:encode)
|
79
|
+
line.encode!('UTF-8', 'UTF-8', :invalid => :replace)
|
80
|
+
else
|
81
|
+
line
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse(line)
|
86
|
+
return $1 if encoded_line(line) =~ DependencyResolver.depends_pattern
|
87
|
+
|
88
|
+
# end of first commenting block we're done.
|
89
|
+
throw :done unless line =~ /--/
|
90
|
+
end
|
91
|
+
|
92
|
+
def relative_path(dep,file)
|
93
|
+
p = Pathname.new(file).dirname.join(dep)
|
94
|
+
if p.exist? && p.extname.present?
|
95
|
+
return p.to_s
|
96
|
+
elsif p.extname.empty?
|
97
|
+
%w(.sql .erb).each do |ext|
|
98
|
+
new_p = p.sub_ext(ext)
|
99
|
+
return new_p.to_s if new_p.exist?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
raise DependencyNotFound.new(dep, file)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class EnumType < Type
|
3
|
+
attr_accessor :labels
|
4
|
+
|
5
|
+
|
6
|
+
def load_attributes
|
7
|
+
super
|
8
|
+
|
9
|
+
res = execute <<-SQL
|
10
|
+
SELECT enumlabel
|
11
|
+
FROM pg_enum
|
12
|
+
WHERE enumtypid = #{oid}
|
13
|
+
ORDER by enumsortorder
|
14
|
+
SQL
|
15
|
+
@labels = res.to_a.map{|r| r['enumlabel']}
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_sql
|
19
|
+
lbl_str = labels.map{|l| "'"+l+"'"}.join(",\n ")
|
20
|
+
|
21
|
+
<<-SQL.gsub(/^ {6}/, '')
|
22
|
+
CREATE TYPE #{name} AS ENUM (
|
23
|
+
#{lbl_str}
|
24
|
+
);
|
25
|
+
SQL
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class Extension
|
3
|
+
attr_reader :name, :version
|
4
|
+
def initialize(name = nil, version = nil)
|
5
|
+
@name = name || File.read('Makefile')[/EXTENSION *= *([^\s]*)/,1]
|
6
|
+
@version = version || File.read("#{self.name}.control")[/default_version *= *'([^']*)'/,1]
|
7
|
+
end
|
8
|
+
|
9
|
+
# main releases without migrations
|
10
|
+
def releases
|
11
|
+
Dir.glob("#{name}--*.sql").reject{|f| f=~/\d--\d/}
|
12
|
+
end
|
13
|
+
|
14
|
+
def available_versions
|
15
|
+
versions = releases.map{|f| ExtensionVersion.new f[/#{name}--([\d\.]*?)\.sql/,1] }
|
16
|
+
versions.sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def install
|
20
|
+
execute "DROP EXTENSION IF EXISTS #{name}"
|
21
|
+
execute "CREATE EXTENSION #{name} VERSION '#{version}'"
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def obj_id
|
26
|
+
@obj_id ||= begin
|
27
|
+
result = execute <<-sql
|
28
|
+
SELECT e.extname, e.oid
|
29
|
+
FROM pg_catalog.pg_extension e
|
30
|
+
WHERE e.extname ~ '^(#{name})$'
|
31
|
+
ORDER BY 1;
|
32
|
+
sql
|
33
|
+
result.first['oid']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def objects
|
38
|
+
@objects ||= begin
|
39
|
+
result = execute <<-SQL
|
40
|
+
SELECT classid::pg_catalog.regclass, objid
|
41
|
+
FROM pg_catalog.pg_depend
|
42
|
+
WHERE refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND refobjid = '#{obj_id}' AND deptype = 'e'
|
43
|
+
ORDER BY 1;
|
44
|
+
SQL
|
45
|
+
|
46
|
+
result.map{|r| PgObject.new(r['objid']).get(r['classid'])}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def types
|
51
|
+
objects.select{|o| o.kind_of?(Type)}
|
52
|
+
end
|
53
|
+
|
54
|
+
def functions
|
55
|
+
objects.select{|o| o.kind_of?(Function)}
|
56
|
+
end
|
57
|
+
|
58
|
+
def casts
|
59
|
+
objects.select{|o| o.kind_of?(Cast)}
|
60
|
+
end
|
61
|
+
|
62
|
+
def operators
|
63
|
+
objects.select{|o| o.kind_of?(Operator)}
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def execute(sql)
|
69
|
+
ActiveRecord::Base.connection.execute sql
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class ExtensionMigrator
|
3
|
+
attr_reader :old_version, :new_version, :name
|
4
|
+
TYPES = [:types, :functions, :casts, :operators]
|
5
|
+
|
6
|
+
def initialize(name, old_version, new_version)
|
7
|
+
@name = name
|
8
|
+
@old_version = Extension.new(name, old_version).install
|
9
|
+
@old_version.objects
|
10
|
+
@new_version = Extension.new(name, new_version).install
|
11
|
+
@new_version.objects
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
File.open("#{name}--#{old_version.version}--#{new_version.version}.sql",'w') do |f|
|
16
|
+
f.puts upgrade
|
17
|
+
end
|
18
|
+
File.open("#{name}--#{new_version.version}--#{old_version.version}.sql",'w') do |f|
|
19
|
+
f.puts downgrade
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def upgrade
|
24
|
+
TYPES.map do |type|
|
25
|
+
diff = object_diff(type,:upgrade)
|
26
|
+
"----#{type}----\n" + diff if diff.present?
|
27
|
+
end.compact.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def downgrade
|
31
|
+
TYPES.map do |type|
|
32
|
+
diff = object_diff(type,:downgrade)
|
33
|
+
"----#{type}----\n" + diff if diff.present?
|
34
|
+
end.compact.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def function_diff
|
38
|
+
object_diff(@old_version.functions, @new_version.functions)
|
39
|
+
end
|
40
|
+
|
41
|
+
def cast_diff
|
42
|
+
object_diff(@old_version.casts, @new_version.casts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def object_diff(type, dir)
|
46
|
+
ids = @old_version.public_send(type).map(&:identify) | @new_version.public_send(type).map(&:identify)
|
47
|
+
|
48
|
+
sqls = ids.map do |id|
|
49
|
+
n = @new_version.public_send(type).find{|n| n.identify == id }
|
50
|
+
o = @old_version.public_send(type).find{|n| n.identify == id }
|
51
|
+
if n
|
52
|
+
n.public_send(dir,o)
|
53
|
+
elsif o
|
54
|
+
o.public_send(dir,o)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sqls.join("\n----\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def execute(sql)
|
63
|
+
ActiveRecord::Base.connection.execute sql
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class ExtensionVersion
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :major, :minor, :patch
|
6
|
+
|
7
|
+
def initialize(version="")
|
8
|
+
@major, @minor, @patch = version.split(".").map(&:to_i)
|
9
|
+
end
|
10
|
+
|
11
|
+
def <=>(other)
|
12
|
+
return major <=> other.major if ((major <=> other.major) != 0)
|
13
|
+
return minor <=> other.minor if ((minor <=> other.minor) != 0)
|
14
|
+
return patch <=> other.patch if ((patch <=> other.patch) != 0)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.sort
|
18
|
+
self.sort!{|a,b| a <=> b}
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
[major,minor,patch].join('.')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Dumbo
|
2
|
+
class Function < PgObject
|
3
|
+
attr_accessor :name, :result_type, :definition, :type, :arg_types
|
4
|
+
identfied_by :name, :arg_types
|
5
|
+
|
6
|
+
def initialize(oid)
|
7
|
+
super
|
8
|
+
get
|
9
|
+
end
|
10
|
+
|
11
|
+
def get
|
12
|
+
if type == 'agg'
|
13
|
+
Aggregate.new(oid)
|
14
|
+
else
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def drop
|
20
|
+
"DROP FUNCTION #{name} #{arg_types}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def upgrade(other)
|
24
|
+
return self.to_sql if other.nil?
|
25
|
+
|
26
|
+
if other.identify != self.identify
|
27
|
+
raise "Not the Same Objects!"
|
28
|
+
end
|
29
|
+
|
30
|
+
return nil if other.to_sql == self.to_sql
|
31
|
+
|
32
|
+
if other.result_type != self.result_type
|
33
|
+
<<-SQL.gsub(/^ {8}/, '')
|
34
|
+
#{self.drop}
|
35
|
+
#{self.to_sql}
|
36
|
+
SQL
|
37
|
+
else
|
38
|
+
self.to_sql
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def downgrade(other)
|
43
|
+
return self.to_sql if other.nil?
|
44
|
+
|
45
|
+
if other.identify != self.identify
|
46
|
+
raise "Not the Same Objects!"
|
47
|
+
end
|
48
|
+
|
49
|
+
return nil if other.to_sql == self.to_sql
|
50
|
+
|
51
|
+
if other.result_type != self.result_type
|
52
|
+
<<-SQL.gsub(/^ {8}/, '')
|
53
|
+
#{self.drop}
|
54
|
+
#{other.to_sql}
|
55
|
+
SQL
|
56
|
+
else
|
57
|
+
other.to_sql
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_attributes
|
62
|
+
result = execute <<-SQL
|
63
|
+
SELECT
|
64
|
+
p.proname as name,
|
65
|
+
pg_catalog.pg_get_function_result(p.oid) as result_type,
|
66
|
+
pg_catalog.pg_get_function_arguments(p.oid) as arg_types,
|
67
|
+
CASE
|
68
|
+
WHEN p.proisagg THEN 'agg'
|
69
|
+
WHEN p.proiswindow THEN 'window'
|
70
|
+
WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
|
71
|
+
ELSE 'normal'
|
72
|
+
END as "type",
|
73
|
+
CASE
|
74
|
+
WHEN p.provolatile = 'i' THEN 'immutable'
|
75
|
+
WHEN p.provolatile = 's' THEN 'stable'
|
76
|
+
WHEN p.provolatile = 'v' THEN 'volatile'
|
77
|
+
END as volatility,
|
78
|
+
proisstrict as is_strict,
|
79
|
+
l.lanname as language,
|
80
|
+
p.prosrc as "source",
|
81
|
+
pg_catalog.obj_description(p.oid, 'pg_proc') as description,
|
82
|
+
CASE WHEN p.proisagg THEN 'agg_dummy' ELSE pg_get_functiondef(p.oid) END as definition
|
83
|
+
|
84
|
+
FROM pg_catalog.pg_proc p
|
85
|
+
LEFT JOIN pg_catalog.pg_language l ON l.oid = p.prolang
|
86
|
+
WHERE pg_catalog.pg_function_is_visible(p.oid)
|
87
|
+
AND p.oid = #{oid};
|
88
|
+
SQL
|
89
|
+
|
90
|
+
result.first.each do |k,v|
|
91
|
+
send("#{k}=",v) rescue nil
|
92
|
+
end
|
93
|
+
|
94
|
+
result.first
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_sql
|
98
|
+
definition.gsub("public.#{name}",name)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|