prick 0.2.0
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 +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
|
+
|