prick 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/create_release.txt +17 -0
- data/doc/flow.txt +98 -0
- data/doc/migra +1 -0
- data/doc/migrations.txt +172 -0
- data/doc/notes.txt +116 -0
- data/doc/sh.prick +316 -0
- data/exe/prick +467 -0
- data/file +0 -0
- data/lib/ext/algorithm.rb +14 -0
- data/lib/ext/fileutils.rb +8 -0
- data/lib/ext/pg.rb +18 -0
- data/lib/prick.rb +21 -0
- data/lib/prick/archive.rb +124 -0
- data/lib/prick/build.rb +376 -0
- data/lib/prick/command.rb +85 -0
- data/lib/prick/constants.rb +199 -0
- data/lib/prick/database.rb +58 -0
- data/lib/prick/dsort.rb +151 -0
- data/lib/prick/ensure.rb +119 -0
- data/lib/prick/exceptions.rb +13 -0
- data/lib/prick/git.rb +159 -0
- data/lib/prick/migra.rb +22 -0
- data/lib/prick/migration.rb +230 -0
- data/lib/prick/project.rb +444 -0
- data/lib/prick/rdbms.rb +147 -0
- data/lib/prick/schema.rb +100 -0
- data/lib/prick/version.rb +133 -0
- data/make_releases +369 -0
- data/prick.gemspec +46 -0
- data/share/features/diff.sql +2 -0
- data/share/features/feature/diff.sql +2 -0
- data/share/features/feature/migrate.sql +2 -0
- data/share/features/features.sql +2 -0
- data/share/features/features.yml +2 -0
- data/share/features/migrations.sql +4 -0
- data/share/gitignore +2 -0
- data/share/schemas/prick/data.sql +8 -0
- data/share/schemas/prick/schema.sql +20 -0
- metadata +188 -0
@@ -0,0 +1,444 @@
|
|
1
|
+
|
2
|
+
require "prick/constants.rb"
|
3
|
+
|
4
|
+
require "prick/archive.rb"
|
5
|
+
require "prick/build.rb"
|
6
|
+
require "prick/migration.rb"
|
7
|
+
require "prick/database.rb"
|
8
|
+
require "prick/schema.rb"
|
9
|
+
require "prick/git.rb"
|
10
|
+
require "prick/migra.rb"
|
11
|
+
|
12
|
+
require 'fileutils'
|
13
|
+
|
14
|
+
module Prick
|
15
|
+
class Project
|
16
|
+
# Name of project. Used in database and release names. Defaults to the name
|
17
|
+
# of the current directory
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
# Name of Postgresql user that own the database(s). Defaults to #name
|
21
|
+
attr_reader :user
|
22
|
+
|
23
|
+
# Schema. Represents the schema definition under the schemas/ directory
|
24
|
+
attr_reader :schema
|
25
|
+
|
26
|
+
# The current release, prerelease or feature
|
27
|
+
def release() @builds_by_name[Git.current_branch] end
|
28
|
+
|
29
|
+
# The project database. The project database has the same name as the project
|
30
|
+
# and doesn't include a version. It does not have to exist
|
31
|
+
attr_reader :database
|
32
|
+
|
33
|
+
# List of all builds
|
34
|
+
def builds() @builds_by_name.values end
|
35
|
+
|
36
|
+
# List of releases ordered from oldest to newest. Custom releases are sorted
|
37
|
+
# after regular releases and alphabetical between themselves
|
38
|
+
attr_reader :releases
|
39
|
+
|
40
|
+
# List of pre-releases
|
41
|
+
attr_reader :prereleases
|
42
|
+
|
43
|
+
# Hash from base-release name to feature. Note that this doesn't follow release history
|
44
|
+
attr_reader :features
|
45
|
+
|
46
|
+
# Ignored and orphaned objects
|
47
|
+
attr_reader :ignored_feature_nodes
|
48
|
+
attr_reader :ignored_release_nodes
|
49
|
+
attr_reader :orphan_feature_nodes
|
50
|
+
attr_reader :orphan_release_nodes
|
51
|
+
attr_reader :orphan_git_branches
|
52
|
+
|
53
|
+
# True if we're on a release branch
|
54
|
+
def release?() release && release.version.pre.nil? && !feature? end
|
55
|
+
|
56
|
+
# True if we're on a pre-release branch
|
57
|
+
def prerelease?() release && !release.version.pre.nil? && !feature? end
|
58
|
+
|
59
|
+
# True if we're on a feature branch
|
60
|
+
def feature?() release && !release.version.feature.nil? end
|
61
|
+
|
62
|
+
# Sorted list of databases associated with a release. Also include the
|
63
|
+
# project database if project. is true. If all: is true, also list
|
64
|
+
# databases that match the project but is without an associated release
|
65
|
+
def databases(all: false, project: true)
|
66
|
+
r = (project ? [database] : [])
|
67
|
+
if all
|
68
|
+
r + Rdbms.list_databases(Prick.database_re(name)).map { |name| Database.new(name, user) }
|
69
|
+
else
|
70
|
+
r + @releases.map(&:database)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# `name` is the project name and `user` is the name of the Postgresql user.
|
75
|
+
# `name` defaults to the name of the current directory and `user` defaults
|
76
|
+
# to `name`
|
77
|
+
def initialize(name = nil, user = nil)
|
78
|
+
@name = name || File.basename(Dir.getwd)
|
79
|
+
@name =~ PROJECT_NAME_RE or raise Error, "Illegal project name: #@name"
|
80
|
+
@user = user || @name
|
81
|
+
@user =~ USER_NAME_RE or raise Error, "Illegal postgres user name: #@user"
|
82
|
+
# Rdbms.ensure_state(:user_exist, @user)
|
83
|
+
Rdbms.create_user(@user) if !Rdbms.exist_user?(@user) # FIXME
|
84
|
+
@schema = Schema.new(self)
|
85
|
+
@database = Database.new(@name, @user)
|
86
|
+
|
87
|
+
@builds_by_name = {}
|
88
|
+
@builds_by_version = {}
|
89
|
+
|
90
|
+
@releases = []
|
91
|
+
@prereleases = []
|
92
|
+
@features = {}
|
93
|
+
|
94
|
+
@ignored_feature_nodes = []
|
95
|
+
@ignored_release_nodes = []
|
96
|
+
|
97
|
+
@orphan_feature_nodes = []
|
98
|
+
@orphan_release_nodes = [] # Includes prereleases
|
99
|
+
|
100
|
+
@orphan_git_branches = []
|
101
|
+
|
102
|
+
load_project
|
103
|
+
end
|
104
|
+
|
105
|
+
def [](name_or_version)
|
106
|
+
name = version = name_or_version
|
107
|
+
case name_or_version
|
108
|
+
when String; @builds_by_name[name]
|
109
|
+
when Version; @builds_by_version[version]
|
110
|
+
when NilClass; nil
|
111
|
+
else
|
112
|
+
raise Internal, "Expected String or Version index, got #{name_or_version.class}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def []=(name_or_version, branch)
|
117
|
+
case name_or_version
|
118
|
+
when String;
|
119
|
+
name = name_or_version
|
120
|
+
version = Version.new(name)
|
121
|
+
when Version
|
122
|
+
version = name_or_version
|
123
|
+
name = version.to_s
|
124
|
+
else
|
125
|
+
raise Internal, "Expected String or Version index, got #{name_or_version.class}"
|
126
|
+
end
|
127
|
+
@builds_by_name[name] = @builds_by_version[version] = branch
|
128
|
+
end
|
129
|
+
|
130
|
+
def dirty?() !Git.clean? end
|
131
|
+
|
132
|
+
def build?(name_or_version)
|
133
|
+
name = version = name_or_version
|
134
|
+
case name_or_version
|
135
|
+
when String; @builds_by_name.key?(name)
|
136
|
+
when Version; @builds_by_version.key?(version)
|
137
|
+
when NilClass; nil
|
138
|
+
else
|
139
|
+
raise Internal, "Expected String or Version index, got #{name_or_version.class}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Initialize an on-disk prick instance
|
144
|
+
def self.initialize_directory(directory)
|
145
|
+
FileUtils.mkdir_p(directory)
|
146
|
+
Dir.chdir(directory) {
|
147
|
+
DIRS.each { |dir|
|
148
|
+
!File.exist?(dir) or raise Fail, "Already initialized: Directory '#{dir}' exists"
|
149
|
+
}
|
150
|
+
|
151
|
+
FileUtils.mkdir_p(DIRS)
|
152
|
+
DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
# Create an initial release 0.0.0. ::initialize_project expects to be executed
|
157
|
+
# in the project directory
|
158
|
+
def self.initialize_project(name = File.basename(Dir.getwd), user = name || ENV['USER'])
|
159
|
+
FileUtils.cp("#{SHARE_PATH}/gitignore", ".gitignore")
|
160
|
+
FileUtils.cp("#{SHARE_PATH}/schemas/prick/schema.sql", "schemas/prick")
|
161
|
+
FileUtils.cp("#{SHARE_PATH}/schemas/prick/data.sql", "schemas/prick")
|
162
|
+
Git.init
|
163
|
+
Git.add(".")
|
164
|
+
Git.commit("Initial import")
|
165
|
+
project = Project.new(name, user)
|
166
|
+
release = Release.new(project, nil, Version.new("0.0.0"))
|
167
|
+
release.create
|
168
|
+
|
169
|
+
Git.delete_branch("master")
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns the full path to the project directory (TODO: Test)
|
173
|
+
def path() File.expand_path(".") end
|
174
|
+
|
175
|
+
### COMMON METHODS
|
176
|
+
|
177
|
+
def checkout(name)
|
178
|
+
check_clean
|
179
|
+
Git.checkout_branch(name)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Migrate the database from its current version to the current release on disk
|
183
|
+
def migrate
|
184
|
+
check_clean
|
185
|
+
database.exist? or raise "Project database not found"
|
186
|
+
history = release.history.keys.reverse.drop(1)
|
187
|
+
history.select! { |release| release.version >= database.version }
|
188
|
+
history.each { |release| release.migration.migrate(database.name) }
|
189
|
+
database.version = release.version
|
190
|
+
end
|
191
|
+
|
192
|
+
### DEVELOPER-LEVEL METHODS
|
193
|
+
|
194
|
+
# Create and switch to feature branch
|
195
|
+
def feature(name)
|
196
|
+
check_clean
|
197
|
+
release? or raise Error, "Need to be on a release branch to create a feature"
|
198
|
+
feature = Feature.new(self, release, name).ensure(:active)
|
199
|
+
Git.commit "Created feature #{name}"
|
200
|
+
end
|
201
|
+
|
202
|
+
def rebase(version)
|
203
|
+
check_clean
|
204
|
+
feature? or raise Error, "Need to be on a feature branch to rebase"
|
205
|
+
release.rebase(version)
|
206
|
+
Git.commit "Rebased to #{version}"
|
207
|
+
end
|
208
|
+
|
209
|
+
### RELEASE MANAGER LEVEL METHODS
|
210
|
+
|
211
|
+
# Create the first prerelease
|
212
|
+
#
|
213
|
+
# TODO Make it just copy files and commit them. Supports a single-user
|
214
|
+
# workflow. Requires a :prepared state
|
215
|
+
def prepare_release
|
216
|
+
check_clean
|
217
|
+
release? or raise Error, "Need to be on a release branch to prepare a new release"
|
218
|
+
release.prepare
|
219
|
+
end
|
220
|
+
|
221
|
+
def prepare_migration
|
222
|
+
check_clean
|
223
|
+
prerelease? || feature? or raise Error, "Need to be on a prerelease or feature branch to include features"
|
224
|
+
|
225
|
+
base_database = release.base_release.database
|
226
|
+
build(base_release.version) if !base_database.loaded?
|
227
|
+
build # if !database.loaded?
|
228
|
+
|
229
|
+
diff_sql = release.migration.diff_sql
|
230
|
+
Migra.migrate(base_database.name, database.name, diff_sql)
|
231
|
+
end
|
232
|
+
|
233
|
+
def create_release(version = nil)
|
234
|
+
check_clean
|
235
|
+
if release?
|
236
|
+
!version.nil? or raise Error, "Need version argument when on a release branch"
|
237
|
+
new_release = Release.new(self, release, version)
|
238
|
+
elsif prerelease?
|
239
|
+
version.nil? or raise Error, "Can't use version argument when on a pre-release branch"
|
240
|
+
new_release = release.target_release
|
241
|
+
else
|
242
|
+
raise Error, "Need to be on a release or pre-prelease branch to create a new release"
|
243
|
+
end
|
244
|
+
new_release.create
|
245
|
+
new_release
|
246
|
+
end
|
247
|
+
|
248
|
+
def create_prerelease(target_version)
|
249
|
+
check_clean
|
250
|
+
release? or raise Error, "Need to be on a release branch to create a pre-release"
|
251
|
+
prerelease = Prerelease.new(self, release, target_version.increment(:pre, 0), target_version)
|
252
|
+
prerelease.create
|
253
|
+
end
|
254
|
+
|
255
|
+
def increment_prerelease
|
256
|
+
check_clean
|
257
|
+
prerelease? or raise Error, "Need to be on a pre-release branch to make an incremental pre-release"
|
258
|
+
prerelease = Prerelease.new(self, release.base_release, release.version.increment(:pre))
|
259
|
+
prerelease.create
|
260
|
+
end
|
261
|
+
|
262
|
+
def create_feature(name)
|
263
|
+
check_clean
|
264
|
+
release? or raise Error, "Need to be on a release branch to create a feature"
|
265
|
+
feature = Feature.new(self, release, name)
|
266
|
+
feature.create
|
267
|
+
end
|
268
|
+
|
269
|
+
# Note: Does not commit
|
270
|
+
def include_feature(feature_version)
|
271
|
+
check_clean
|
272
|
+
prerelease? || feature? or raise Error, "Need to be on a prerelease or feature branch to include features"
|
273
|
+
Git.branch?(feature_version) or raise Error, "Can't find branch #{feature_version}"
|
274
|
+
release.include_feature(self[feature_version])
|
275
|
+
end
|
276
|
+
|
277
|
+
def commit_feature
|
278
|
+
msg = File.readlines(".git/MERGE_MSG").first.chomp
|
279
|
+
Git.commit msg
|
280
|
+
# Command.command "git commit -F .git/MERGE_MSG"
|
281
|
+
end
|
282
|
+
|
283
|
+
def create_migration
|
284
|
+
end
|
285
|
+
|
286
|
+
# Build the current database from the content of schema
|
287
|
+
def build(version = nil)
|
288
|
+
if version.nil?
|
289
|
+
database.recreate
|
290
|
+
schema.build
|
291
|
+
else
|
292
|
+
release = self[version] or raise Error, "Can't find release #{version}"
|
293
|
+
release.database.recreate
|
294
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
295
|
+
if !File.directory?(File.join(CLONE_DIR, ".git"))
|
296
|
+
FileUtils.rm_rf(CLONE_DIR)
|
297
|
+
Command.command "git clone . #{CLONE_DIR}"
|
298
|
+
end
|
299
|
+
Dir.chdir(CLONE_DIR) {
|
300
|
+
Git.checkout_tag(version.to_s)
|
301
|
+
project = Project.new(name, user)
|
302
|
+
release.database.recreate
|
303
|
+
project.schema.build(release.database)
|
304
|
+
}
|
305
|
+
release.cache
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Load file into current database
|
310
|
+
def use_database(file = nil)
|
311
|
+
check_clean
|
312
|
+
File.file?(file) or raise Error, "Can't find #{file}"
|
313
|
+
database.recreate
|
314
|
+
database.load(file)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Load file into the given database version. If version is nil the current
|
318
|
+
# database is used. If file is nil, the database associated with the given
|
319
|
+
# version is used. Not both version and file can be nil
|
320
|
+
def load_database(version, file = nil)
|
321
|
+
check_clean
|
322
|
+
!version.nil? || !file.nil? or raise Fail, "Not both version and file can be nil"
|
323
|
+
if version
|
324
|
+
release = self[version]
|
325
|
+
file ||= release.archive.path
|
326
|
+
File.file?(file) or raise Error, "Can't find #{file}"
|
327
|
+
release.database.recreate
|
328
|
+
release.database.load(file)
|
329
|
+
else
|
330
|
+
database.recreate
|
331
|
+
database.load(file)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# def reload_release(version, *file)
|
336
|
+
# lookup(version).reload(*file)
|
337
|
+
# end
|
338
|
+
#
|
339
|
+
# def unload(version = nil)
|
340
|
+
# if version
|
341
|
+
# lookup(version).database.drop
|
342
|
+
# else
|
343
|
+
# databases.each(&:drop)
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# Remove temporary files and databases
|
348
|
+
def cleanup(version = nil, project: false)
|
349
|
+
if version
|
350
|
+
release = self[version]
|
351
|
+
release.archive.delete
|
352
|
+
release.database.drop
|
353
|
+
else
|
354
|
+
FileUtils::rm_rf CLONE_DIR
|
355
|
+
FileUtils::rm_f Dir.glob(File.join(CACHE_DIR, Prick.dump_glob(name)))
|
356
|
+
databases(all: true, project: project).each(&:drop)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
def check_clean()
|
362
|
+
!dirty? or raise "Repository is dirty. Commit your changes and try again"
|
363
|
+
end
|
364
|
+
|
365
|
+
# For debug
|
366
|
+
def dump_deps(deps)
|
367
|
+
deps.each { |k,v|
|
368
|
+
puts " #{k} -> #{v || 'nil'}"
|
369
|
+
}
|
370
|
+
end
|
371
|
+
|
372
|
+
def load_project
|
373
|
+
# Maps from version to base release version
|
374
|
+
releases = { Version.zero => nil }
|
375
|
+
prereleases = {}
|
376
|
+
features = {}
|
377
|
+
|
378
|
+
# Collect branches from git
|
379
|
+
Git.list_branches.each { |branch|
|
380
|
+
if Version.version?(branch)
|
381
|
+
version = Version.new(branch)
|
382
|
+
if version.feature?
|
383
|
+
features[version] = version.truncate(:feature)
|
384
|
+
elsif version.pre?
|
385
|
+
prereleases[version] = nil
|
386
|
+
elsif version.release?
|
387
|
+
releases[version] = nil
|
388
|
+
else
|
389
|
+
raise Internal, "Unexpected state for #{version}"
|
390
|
+
end
|
391
|
+
else
|
392
|
+
@orphan_git_branches << branch
|
393
|
+
end
|
394
|
+
}
|
395
|
+
|
396
|
+
# Find base release for present releases and prereleases
|
397
|
+
Dir.each_child(RELEASE_DIR) { |node|
|
398
|
+
next if node.start_with?(".")
|
399
|
+
if Version.version?(node)
|
400
|
+
version = Version.new(node)
|
401
|
+
if releases.key?(version)
|
402
|
+
base_release = Build.deref_node_file(File.join(RELEASE_DIR, node))
|
403
|
+
releases[version] = base_release
|
404
|
+
elsif prereleases.key?(version)
|
405
|
+
base_release = Build.deref_node_file(File.join(RELEASE_DIR, node))
|
406
|
+
prereleases[version] = base_release
|
407
|
+
else
|
408
|
+
@orphan_release_nodes << node
|
409
|
+
end
|
410
|
+
else !prerelease_deps.key?(node)
|
411
|
+
@ignored_release_nodes << node
|
412
|
+
end
|
413
|
+
}
|
414
|
+
|
415
|
+
releases.sort_by { |k,v| k }.each { |release, base_release|
|
416
|
+
@releases << Release.new(self, self[base_release], release)
|
417
|
+
}
|
418
|
+
@releases.sort!
|
419
|
+
|
420
|
+
prereleases.each { |prerelease, base_release|
|
421
|
+
if base_release
|
422
|
+
@prereleases << Prerelease.new(self, self[base_release], prerelease)
|
423
|
+
else
|
424
|
+
@ignored_release_nodes << prerelease
|
425
|
+
end
|
426
|
+
}
|
427
|
+
@prereleases.sort!
|
428
|
+
|
429
|
+
if !Git.detached?
|
430
|
+
features.each { |feature, base_release|
|
431
|
+
# if File.exist?(base_release_file)
|
432
|
+
# recursive include of dependent releases
|
433
|
+
|
434
|
+
if base_release
|
435
|
+
(@features[base_release] ||= []) << Feature.new(self, self[base_release], feature.feature)
|
436
|
+
else
|
437
|
+
@ignored_release_nodes << release
|
438
|
+
end
|
439
|
+
}
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
data/lib/prick/rdbms.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'prick/command.rb'
|
2
|
+
require 'prick/ensure.rb'
|
3
|
+
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module Prick
|
7
|
+
module Rdbms
|
8
|
+
extend Ensure
|
9
|
+
|
10
|
+
### EXECUTE SQL
|
11
|
+
|
12
|
+
# Execute the SQL statement and return stdout as an array of tuples
|
13
|
+
def self.exec_sql(db, sql, user: ENV['USER'])
|
14
|
+
stdout = Command.command %(
|
15
|
+
{
|
16
|
+
echo "set role #{user};"
|
17
|
+
echo "set search_path to public;"
|
18
|
+
echo "#{sql}"
|
19
|
+
} | psql --csv --tuples-only --quiet -v ON_ERROR_STOP=1 -d #{db}
|
20
|
+
)
|
21
|
+
CSV.new(stdout.join("\n")).read
|
22
|
+
end
|
23
|
+
|
24
|
+
# Execute the given file and return stdout as an array of tuples
|
25
|
+
def self.exec_file(db, file, user: ENV['USER'])
|
26
|
+
self.exec_sql(db, File.read(file), user: user)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Execute the SQL statement and return the result as an array of record tuples
|
30
|
+
# Just an alias for ::exec_sql
|
31
|
+
def self.select(db, sql, user: ENV['USER']) exec_sql(db, sql, user: user) end
|
32
|
+
|
33
|
+
# Execute the SQL statement and return an array of values, one for each
|
34
|
+
# single-valued record in the result. Raises an exception if the SQL
|
35
|
+
# statement doesn't return records with exactly one field
|
36
|
+
def self.select_values(db, sql, user: ENV['USER'])
|
37
|
+
records = exec_sql(db, sql, user: user)
|
38
|
+
return [] if records.empty?
|
39
|
+
records.first.size == 1 or raise Prick::Fail, "Expected records with one field"
|
40
|
+
records.flatten
|
41
|
+
end
|
42
|
+
|
43
|
+
# Execute the SQL statement and return a single record as an array of values.
|
44
|
+
# Raises an exception if the SQL statement doesn't return exactly one
|
45
|
+
# record
|
46
|
+
def self.select_record(db, sql, user: ENV['USER'])
|
47
|
+
records = exec_sql(db, sql, user: user)
|
48
|
+
records.size == 1 or raise Prick::Fail, "Expected one row only"
|
49
|
+
records.first
|
50
|
+
end
|
51
|
+
|
52
|
+
# Execute the SQL statement and return a value. The value is the first
|
53
|
+
# field of the first record in the result. Raises an exception if the SQL
|
54
|
+
# statement doesn't return exactly one record with one field
|
55
|
+
def self.select_value(db, sql, user: ENV['USER'])
|
56
|
+
row = select_record(db, sql, user: user)
|
57
|
+
row.size == 1 or raise Prick::Fail, "Expected one field only"
|
58
|
+
row.first
|
59
|
+
end
|
60
|
+
|
61
|
+
### DETECT SCHEMAS, TABLES, AND RECORDS
|
62
|
+
|
63
|
+
def self.exist_schema?(db, schema)
|
64
|
+
exist_record?(db, %(
|
65
|
+
select 1
|
66
|
+
from information_schema.schemata
|
67
|
+
where catalog_name = '#{db}'
|
68
|
+
and schema_name = '#{schema}'
|
69
|
+
))
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.exist_table?(db, schema, table)
|
73
|
+
exist_record?(db, %(
|
74
|
+
select 1
|
75
|
+
from information_schema.tables
|
76
|
+
where table_schema = '#{schema}'
|
77
|
+
and table_name = '#{table}'
|
78
|
+
))
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.exist_record?(db, sql)
|
82
|
+
!select(db, sql).empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
### MAINTAIN USERS AND DATABASES
|
86
|
+
|
87
|
+
def self.exist_user?(user)
|
88
|
+
exist_record?("template1", "select 1 from pg_roles where rolname = '#{user}'")
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.create_user(user)
|
92
|
+
Command.command("createuser #{user}")
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.drop_user(user, fail: true)
|
96
|
+
Command.command "dropuser #{user}", fail: fail
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.exist_database?(db)
|
100
|
+
exist_record?("template1", "select 1 from pg_database where datname = '#{db}'")
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO: make `owner` an option
|
104
|
+
def self.create_database(db, owner: ENV['USER'], template: "template1")
|
105
|
+
owner_option = (owner ? "-O #{owner}" : "")
|
106
|
+
Command.command "createdb -T #{template} #{owner_option} #{db}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.copy_database(from, to, owner: ENV['USER'])
|
110
|
+
create_database(to, owner: owner, template: from)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.drop_database(db, fail: true)
|
114
|
+
Command.command "dropdb #{db}", fail: fail
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.list_databases(re = /.*/)
|
118
|
+
select_values("template1", "select datname from pg_database").select { |db| db =~ re }
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.load(db, file, user: ENV['USER'])
|
122
|
+
Command.command %(
|
123
|
+
{
|
124
|
+
echo "set role #{user};"
|
125
|
+
gunzip --to-stdout #{file}
|
126
|
+
} | psql -v ON_ERROR_STOP=1 -d #{db}
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.save(db, file, data: true)
|
131
|
+
data_opt = (data ? "" : "--schema-only")
|
132
|
+
Command.command "pg_dump --no-owner #{data_opt} #{db} | gzip --to-stdout >#{file}"
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
@ensure_states = {
|
137
|
+
exist_user: [:create_user, :drop_user],
|
138
|
+
exist_database: [
|
139
|
+
lambda { |this, db, user| this.ensure_state(:exist_user, user) },
|
140
|
+
lambda { |this, db, user| this.create_database(db, owner: user) },
|
141
|
+
:drop_database
|
142
|
+
]
|
143
|
+
}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|