dumbo 0.0.1

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