dumbo 0.0.1
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.
- 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
|