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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +61 -0
  6. data/Rakefile +9 -0
  7. data/bin/dumbo +58 -0
  8. data/config/boot.rb +15 -0
  9. data/config/database.yml +31 -0
  10. data/dumbo.gemspec +31 -0
  11. data/lib/dumbo.rb +21 -0
  12. data/lib/dumbo/aggregate.rb +57 -0
  13. data/lib/dumbo/base_type.rb +71 -0
  14. data/lib/dumbo/cast.rb +49 -0
  15. data/lib/dumbo/composite_type.rb +31 -0
  16. data/lib/dumbo/db_task.rb +57 -0
  17. data/lib/dumbo/dependency_resolver.rb +105 -0
  18. data/lib/dumbo/enum_type.rb +28 -0
  19. data/lib/dumbo/extension.rb +73 -0
  20. data/lib/dumbo/extension_migrator.rb +66 -0
  21. data/lib/dumbo/extension_version.rb +25 -0
  22. data/lib/dumbo/function.rb +101 -0
  23. data/lib/dumbo/operator.rb +74 -0
  24. data/lib/dumbo/pg_object.rb +80 -0
  25. data/lib/dumbo/rake_task.rb +121 -0
  26. data/lib/dumbo/range_type.rb +43 -0
  27. data/lib/dumbo/type.rb +31 -0
  28. data/lib/dumbo/version.rb +3 -0
  29. data/lib/tasks/db.rake +52 -0
  30. data/lib/tasks/dumbo.rake +23 -0
  31. data/spec/Makefile +6 -0
  32. data/spec/aggregate_spec.rb +41 -0
  33. data/spec/cast_spec.rb +20 -0
  34. data/spec/dumbo_sample--0.0.1.sql +5 -0
  35. data/spec/dumbo_sample--0.0.2.sql +5 -0
  36. data/spec/dumbo_sample.control +5 -0
  37. data/spec/extension_migrator_spec.rb +40 -0
  38. data/spec/extension_spec.rb +19 -0
  39. data/spec/operator_spec.rb +42 -0
  40. data/spec/spec_helper.rb +28 -0
  41. data/spec/support/sql_helper.rb +23 -0
  42. data/spec/type_spec.rb +95 -0
  43. data/template/Gemfile +3 -0
  44. data/template/Makefile.erb +6 -0
  45. data/template/Rakefile +15 -0
  46. data/template/config/database.yml.erb +31 -0
  47. data/template/spec/sample_spec.rb.erb +13 -0
  48. data/template/sql/sample.sql +5 -0
  49. 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