pgbundle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module PgBundle
2
+ # The Dsl class defines the Domain Specific Language for the PgFile
3
+ # it's mainly user to parse a PgFile and return a Definition Object
4
+ class Dsl
5
+ def initialize
6
+ @definition = Definition.new
7
+ @extensions = []
8
+ end
9
+
10
+ def eval_pgfile(pgfile, contents=nil)
11
+ contents ||= File.read(pgfile.to_s)
12
+ instance_eval(contents)
13
+ @definition
14
+ rescue SyntaxError => e
15
+ syntax_msg = e.message.gsub("#{pgfile}:", 'on line ')
16
+ raise PgfileError, "Pgfile syntax error #{syntax_msg}"
17
+ rescue ScriptError, RegexpError, NameError, ArgumentError => e
18
+ e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})"
19
+ puts e.backtrace.join("\n ")
20
+ raise PgfileError, "There was an error in your Pgfile," \
21
+ " and pgbundle cannot continue. " \
22
+ + e.message
23
+ end
24
+
25
+ def database(*args)
26
+ opts = extract_options!(args)
27
+ @definition.database = Database.new(args.first, opts)
28
+ end
29
+
30
+ def pgx(*args)
31
+ opts = extract_options!(args)
32
+ ext = Extension.new(*args, opts)
33
+ @definition.extensions[ext.name] = ext
34
+ end
35
+
36
+ def extract_options!(arr)
37
+ if arr.last.is_a? Hash
38
+ arr.pop
39
+ else
40
+ {}
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,270 @@
1
+ require 'net/ssh'
2
+ module PgBundle
3
+ # The Extension class provides the api for defining an Extension
4
+ # it installation source, and dependencies
5
+ # example:
6
+ # define an extension named 'foo' at version '0.1.1', with source on github depending on hstore
7
+ # Extension.new('foo', '0.1.1', github: 'me/foo', requires: 'hstore')
8
+ # you can then check if the Extension is available on a given database
9
+ # extension.available?(database)
10
+ # or install it along with it's dependencies
11
+ # extension.install(database)
12
+ class Extension
13
+ attr_accessor :name, :version, :source, :resolving_dependencies
14
+ def initialize(*args)
15
+ opts = args.last.is_a?(Hash) ? args.pop : {}
16
+ @name, @version = args
17
+ self.dependencies = opts[:requires]
18
+ set_source(opts)
19
+ end
20
+
21
+ def dependencies
22
+ @dependencies
23
+ end
24
+
25
+ # set dependency hash with different options
26
+ # dependencies= {foo: Extension.new('foo'), bar: Extension.new('bar')}
27
+ # => {'foo' => Extension.new('foo'), 'bar' Extension.new('bar')}
28
+ # dependencies= 'foo'
29
+ # => {foo: Extension.new('foo')}
30
+ # dependencies= Extension.new('foo')
31
+ # => {foo: Extension.new('foo')}
32
+ # dependencies= ['foo', 'bar']
33
+ # => {'foo' => Extension.new('foo'), 'bar' Extension.new('bar')}
34
+ def dependencies=(obj = nil)
35
+ @dependencies = case obj
36
+ when nil
37
+ {}
38
+ when Hash
39
+ Hash[obj.map { |k, v| [k.to_s, v] }]
40
+ when String, Symbol
41
+ { obj.to_s => Extension.new(obj.to_s) }
42
+ when Extension
43
+ { obj.name => obj }
44
+ when Array
45
+ hashable = obj.map do |o|
46
+ case o
47
+ when String, Symbol
48
+ [o.to_s, Extension.new(obj.to_s)]
49
+ when Extension
50
+ [o.name, o]
51
+ end
52
+ end
53
+ Hash[hashable]
54
+ end
55
+ end
56
+
57
+ # returns true if extension is available for installation on a given database
58
+ def available?(database)
59
+ return false unless installed?(database)
60
+ return true if created?(database)
61
+
62
+ created_any_version?(database) ? updatable?(database) : creatable?(database)
63
+ rescue ExtensionCreateError
64
+ false
65
+ end
66
+
67
+ # returns true if extension is already created with the correct version in the given database
68
+ def created?(database)
69
+ if version
70
+ result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND version = '#{version}' AND installed").to_a
71
+ else
72
+ result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND installed").to_a
73
+ end
74
+
75
+ if result.empty?
76
+ false
77
+ else
78
+ true
79
+ end
80
+ end
81
+
82
+ # returns if the extension is already installed on the database system
83
+ # if it is also already created it returns the installed version
84
+ def installed?(database)
85
+ if version
86
+ result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND version = '#{version}'").to_a
87
+ else
88
+ result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}'").to_a
89
+ end
90
+
91
+ if result.empty?
92
+ false
93
+ else
94
+ true
95
+ end
96
+ end
97
+
98
+ # checks that all dependencies are installed on a given database
99
+ def dependencies_installed?(database)
100
+ dependencies.all?{|_, d| d.installed?(database)}
101
+ end
102
+
103
+ # installs extension and all dependencies using make install
104
+ # returns true if Extension can successfully be created using CREATE EXTENSION
105
+ def install(database)
106
+ unless dependencies.empty?
107
+ install_dependencies(database)
108
+ end
109
+
110
+ make_install(database)
111
+ raise ExtensionNotFound.new(name, version) unless installed?(database)
112
+
113
+ add_missing_required_dependencies(database)
114
+
115
+ creatable?(database)
116
+ end
117
+
118
+ # completely removes extension be running make uninstall and DROP EXTENSION
119
+ def uninstall!(database)
120
+ drop_extension(database)
121
+ make_uninstall(database)
122
+ end
123
+
124
+ private
125
+
126
+ # create extension on a given database connection
127
+ def create!(con)
128
+ con.exec create_stmt
129
+ end
130
+
131
+ # create the dependency graph on the given connection
132
+ def create_dependencies(con)
133
+ @resolving_dependencies = true
134
+ dependencies.each do |_, d|
135
+ fail CircularDependencyError.new(name, d.name) if d.resolving_dependencies
136
+ d.create_dependencies(con)
137
+ d.create!(con)
138
+ end
139
+ @resolving_dependencies = false
140
+ end
141
+
142
+ # checks if Extension is already installed at any version thus need ALTER EXTENSION to install
143
+ def created_any_version?(database)
144
+ result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND installed").to_a
145
+ if result.empty?
146
+ false
147
+ else
148
+ true
149
+ end
150
+ end
151
+
152
+ # adds dependencies that are required but not defined yet
153
+ def add_missing_required_dependencies(database)
154
+ requires = requires(database)
155
+ requires.each do |name|
156
+ unless dependencies[name]
157
+ dependencies[name] = Extension.new(name)
158
+ end
159
+ end
160
+ end
161
+
162
+ # returns an array of required Extension specified in the extensions control file
163
+ def requires(database)
164
+ fail "Extension #{name} not (yet) installed" unless installed?(database)
165
+
166
+ stmt = if version
167
+ <<-SQL
168
+ SELECT unnest(requires) as name FROM
169
+ ( SELECT requires FROM pg_available_extension_versions where name='#{name}' AND version ='#{version}') t
170
+ SQL
171
+ else
172
+ <<-SQL
173
+ SELECT unnest(requires) as name FROM
174
+ (SELECT requires FROM
175
+ pg_available_extensions a
176
+ JOIN pg_available_extension_versions v ON v.name = a.name AND a.default_version = v.version
177
+ WHERE v.name = '#{name}')t
178
+ SQL
179
+ end
180
+
181
+ result = database.execute(stmt).to_a
182
+
183
+ requires = result.map{|r| r['name']}
184
+ end
185
+
186
+ # loads the source and runs make uninstall
187
+ # returns: self
188
+ def make_uninstall(database)
189
+ database.make_uninstall(source, name)
190
+ self
191
+ end
192
+
193
+ def drop_extension(database)
194
+ database.drop_extension(name)
195
+ end
196
+
197
+ # loads the source and runs make install
198
+ # returns: self
199
+ def make_install(database)
200
+ return self if installed?(database)
201
+
202
+ fail SourceNotFound, name if source.nil?
203
+
204
+ database.make_install(source, name)
205
+ self
206
+ end
207
+
208
+ def create_stmt
209
+ stmt = "CREATE EXTENSION #{name}"
210
+ stmt += " VERSION '#{version}'" unless version.nil? || version.empty?
211
+
212
+ stmt
213
+ end
214
+
215
+ def install_dependencies(database)
216
+ begin
217
+ dependencies.each do |_, d|
218
+ d.install(database)
219
+ end
220
+ rescue InstallError, ExtensionCreateError => e
221
+ raise DependencyNotFound.new(name, e.message)
222
+ end
223
+
224
+ true
225
+ end
226
+
227
+ def creatable?(database)
228
+ dependencies_installed?(database) && installed?(database)
229
+ end
230
+
231
+ # hard checks that the dependency can be created running CREATE command in a transaction
232
+ def creatable!(database)
233
+ database.transaction_rollback do |con|
234
+ begin
235
+ create_dependencies(con)
236
+ create!(con)
237
+ rescue PG::UndefinedFile => err
238
+ raise ExtensionNotFound.new(name, version)
239
+ rescue PG::UndefinedObject => err
240
+ raise MissingDependency.new(name, err.message)
241
+ end
242
+ end
243
+
244
+ true
245
+ end
246
+
247
+ # checks that the extension can be updated running ALTER EXTENSION command in a transaction
248
+ def updatable?(database)
249
+ result = true
250
+ database.execute 'BEGIN'
251
+ begin
252
+ database.execute "ALTER EXTENSION #{name} UPDATE TO '#{version}'"
253
+ rescue PG::UndefinedFile, PG::UndefinedObject => err
254
+ @error = err.message
255
+ result = false
256
+ end
257
+ database.execute 'ROLLBACK'
258
+
259
+ result
260
+ end
261
+
262
+ def set_source(opts)
263
+ if opts[:path]
264
+ @source = PathSource.new(opts[:path])
265
+ elsif opts[:github]
266
+ @source = GithubSource.new(opts[:github])
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,32 @@
1
+ require 'tmpdir'
2
+ module PgBundle
3
+ # The GithubSource class defines a Github Source
4
+ class GithubSource < BaseSource
5
+ def load(host, user, dest)
6
+ clone(dest)
7
+ if host == 'localhost'
8
+ copy_local(clone_dir, dest)
9
+ else
10
+ copy_to_remote(host, user, clone_dir, dest)
11
+ end
12
+ end
13
+
14
+ def branch_name
15
+ @branch || 'master'
16
+ end
17
+
18
+ private
19
+
20
+ def clone(dest)
21
+ # git clone user@git-server:project_name.git -b branch_name /some/folder
22
+ %x((git clone git@github.com:#{path}.git -b #{branch_name} --quiet --depth=1 #{clone_dir} && rm -rf #{clone_dir}/.git}) 2>&1)
23
+ unless $?.success?
24
+ fail GitCommandError
25
+ end
26
+ end
27
+
28
+ def clone_dir
29
+ @clone_dir ||= Dir.mktmpdir
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ require 'tmpdir'
2
+ require 'net/scp'
3
+ module PgBundle
4
+ # The PathSource class defines a local Path Source
5
+ # eg. PathSource.new('/my/local/path')
6
+ class PathSource < BaseSource
7
+ def load(host, user, dest)
8
+ if host == 'localhost'
9
+ copy_local(path, dest)
10
+ else
11
+ copy_to_remote(host, user, path, dest)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Pgbundle
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pgbundle/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pgbundle"
8
+ spec.version = Pgbundle::VERSION
9
+ spec.authors = ["Manuel Kniep"]
10
+ spec.email = ["manuel@adjust.com"]
11
+ spec.summary = %q{bundling postgres extension}
12
+ spec.description = %q{bundler like postgres extension manager}
13
+ spec.homepage = "http://github.com/adjust/pgbundle"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'thor'
22
+ spec.add_dependency 'net-ssh'
23
+ spec.add_dependency 'net-scp'
24
+ #https://bitbucket.org/ged/ruby-pg/wiki/Home
25
+ spec.add_dependency 'pg', '> 0.17'
26
+ spec.add_development_dependency 'rspec', '~> 2.14.0'
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "pry"
30
+ end
@@ -0,0 +1,6 @@
1
+ database 'pgbundle_test', host: 'localhost'
2
+
3
+ pgx 'hstore'
4
+ pgx 'bar', path: './spec/sample_extensions/bar', requires: 'ltree'
5
+ pgx 'baz', '0.0.2', path: './spec/sample_extensions/baz', requires: 'foo'
6
+ pgx 'foo', '0.0.1', path: './spec/sample_extensions/foo'
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ describe PgBundle::Definition do
3
+ subject do
4
+ d = PgBundle::Definition.new
5
+ d.database = database
6
+ d.extensions['bar'] = PgBundle::Extension.new('bar', path: './spec/sample_extensions//bar', requires: 'ltree')
7
+ d.extensions['baz'] = PgBundle::Extension.new('baz', path: './spec/sample_extensions/baz', requires: 'foo')
8
+ d.extensions['foo'] = PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo')
9
+ d
10
+ end
11
+
12
+ it 'missing_extensions' do
13
+ subject.missing_extensions.map(&:name).should eq %w(bar baz foo)
14
+ end
15
+
16
+ it 'should install missing extension' do
17
+ subject.install.map(&:name).should eq %w(bar baz foo)
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe PgBundle::Dsl do
4
+
5
+ subject { PgBundle::Dsl.new.eval_pgfile(File.expand_path('../Pgfile', __FILE__)) }
6
+ its(:database) { should be_a PgBundle::Database }
7
+ its(:extensions) { should be_a Hash }
8
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+ describe PgBundle::Extension do
3
+ describe 'Basiscs' do
4
+ subject { PgBundle::Extension.new('foo', '1.2.3', github: 'bar/foo', requires: 'baz') }
5
+
6
+ its(:name) { should eq 'foo' }
7
+ its(:version) { should eq '1.2.3' }
8
+ its('dependencies.first.last') { should be_a PgBundle::Extension }
9
+ its(:source) { should be_a PgBundle::GithubSource }
10
+ end
11
+
12
+ describe 'Installation' do
13
+
14
+ context 'version available' do
15
+ subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo') }
16
+
17
+ it 'should not be available before install' do
18
+ subject.available?(database).should be false
19
+ end
20
+
21
+ it 'should be available after install' do
22
+ subject.install(database)
23
+ subject.available?(database).should be true
24
+ end
25
+
26
+ context 'wrong version installed' do
27
+ subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo') }
28
+ let(:wrong_version) { PgBundle::Extension.new('foo', '0.0.1', path: './spec/sample_extensions/foo') }
29
+ before do
30
+ wrong_version.install(database)
31
+ database.connection.exec "CREATE EXTENSION foo VERSION '0.0.2'"
32
+ end
33
+
34
+ it 'should be already installed' do
35
+ subject.should be_created_any_version(database)
36
+ end
37
+
38
+ it 'should be available' do
39
+ subject.should be_available(database)
40
+ end
41
+ end
42
+
43
+ context 'with require' do
44
+ let(:dependend) { PgBundle::Extension.new('bar', path: './spec/sample_extensions/bar', requires: 'ltree') }
45
+ subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo', requires: dependend) }
46
+
47
+ it 'requires should be installable' do
48
+ dependend.should_not be_available(database)
49
+ subject.install(database).should be true
50
+ dependend.should be_available(database)
51
+ end
52
+
53
+ end
54
+ end
55
+
56
+ context 'version not available' do
57
+ subject { PgBundle::Extension.new('foo', '0.0.3', path: './spec/sample_extensions/foo') }
58
+
59
+ it 'should raise ExtensionNotFound' do
60
+ expect { subject.install(database) }.to raise_error PgBundle::ExtensionNotFound, 'specified Version 0.0.3 for Extension foo not available'
61
+ end
62
+
63
+ it 'should not be available although it is installed' do
64
+ subject.available?(database).should be false
65
+ end
66
+ end
67
+
68
+ context 'require not found' do
69
+ subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo', requires: PgBundle::Extension.new('noope')) }
70
+
71
+ it 'should raise DependencyNotFound' do
72
+ expect { subject.install(database) }.to raise_error PgBundle::DependencyNotFound, "Can't install Dependency for Extension foo: Source for Extension noope not found"
73
+ end
74
+ end
75
+ end
76
+ end