prick 0.2.0 → 0.3.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 +4 -4
- data/.gitignore +3 -5
- data/Gemfile +4 -1
- data/TODO +3 -0
- data/doc/prick.txt +114 -0
- data/exe/prick +224 -370
- data/lib/ext/fileutils.rb +11 -0
- data/lib/ext/forward_method.rb +18 -0
- data/lib/ext/shortest_path.rb +44 -0
- data/lib/prick.rb +17 -9
- data/lib/prick/branch.rb +254 -0
- data/lib/prick/builder.rb +141 -0
- data/lib/prick/command.rb +19 -11
- data/lib/prick/constants.rb +42 -20
- data/lib/prick/database.rb +5 -3
- data/lib/prick/diff.rb +47 -0
- data/lib/prick/exceptions.rb +15 -3
- data/lib/prick/git.rb +46 -21
- data/lib/prick/migration.rb +165 -185
- data/lib/prick/program.rb +238 -0
- data/lib/prick/project.rb +266 -358
- data/lib/prick/rdbms.rb +2 -2
- data/lib/prick/schema.rb +19 -88
- data/lib/prick/share.rb +64 -0
- data/lib/prick/state.rb +137 -0
- data/lib/prick/version.rb +34 -14
- data/libexec/strip-comments +33 -0
- data/make_releases +48 -345
- data/make_schema +10 -0
- data/prick.gemspec +11 -22
- data/share/feature_migration/diff.sql +2 -0
- data/share/feature_migration/migrate.sql +2 -0
- data/share/release_migration/diff.sql +3 -0
- data/share/release_migration/features.yml +6 -0
- data/share/release_migration/migrate.sql +5 -0
- data/share/release_migration/migrate.yml +5 -0
- data/share/schema/build.yml +14 -0
- data/share/schema/schema.sql +5 -0
- data/share/schemas/build.yml +3 -0
- data/share/schemas/prick/build.yml +14 -0
- data/share/schemas/prick/data.sql +1 -2
- data/share/schemas/prick/schema.sql +0 -15
- data/share/schemas/prick/tables.sql +17 -0
- data/share/schemas/public/.keep +0 -0
- data/share/schemas/public/build.yml +14 -0
- data/share/schemas/public/schema.sql +3 -0
- data/test_assorted +192 -0
- data/test_feature +112 -0
- data/test_single_dev +83 -0
- metadata +34 -61
data/lib/ext/fileutils.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
2
5
|
module FileUtils
|
3
6
|
def self.touch_p(file)
|
4
7
|
dir = File.dirname(file)
|
5
8
|
File.exist?(dir) or FileUtils.mkdir_p(dir)
|
6
9
|
touch(file)
|
7
10
|
end
|
11
|
+
|
12
|
+
def self.ln_sr(from, to)
|
13
|
+
from = Pathname.new(from)
|
14
|
+
to_dir = File.dirname(to)
|
15
|
+
to_file = File.basename(to)
|
16
|
+
relpath = from.relative_path_from(File.dirname(to))
|
17
|
+
Dir.chdir(to_dir) { FileUtils.ln_s(relpath, File.basename(to)) }
|
18
|
+
end
|
8
19
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
class Class
|
3
|
+
# TODO: Check for arity
|
4
|
+
def forward_methods(*methods, object)
|
5
|
+
for method in Array(methods).flatten
|
6
|
+
if method.to_s.end_with?("=")
|
7
|
+
src = "def #{method}(args) raise if #{object}.nil?; #{object}.#{method}(args) end"
|
8
|
+
else
|
9
|
+
src = "def #{method}(*args) #{object}&.#{method}(*args) end"
|
10
|
+
end
|
11
|
+
class_eval(src)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def forward_method(*args) forward_methods(*args) end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module Algorithm
|
3
|
+
# Return (one of) the shortest path from `from` to `to` as a list of edges.
|
4
|
+
# `edges` is a list of arrays with from-node and to-node as the first two
|
5
|
+
# elements. Returns nil if no route was found
|
6
|
+
#
|
7
|
+
# The algorithm does slightly more work than it needs to because the same node
|
8
|
+
# can be inserted more than once into the queue of nodes to be processed
|
9
|
+
#
|
10
|
+
def self.shortest_path(edges, from, to)
|
11
|
+
pool = {} # Map from 'from' to map from 'to' to edge. Eg. pool[from][to] == edge-between-them
|
12
|
+
edges.each { |edge| (pool[edge[0]] ||= {})[edge[1]] = edge }
|
13
|
+
|
14
|
+
visited = {}
|
15
|
+
prev = {}
|
16
|
+
|
17
|
+
todo = [from]
|
18
|
+
while true
|
19
|
+
return nil if todo.empty?
|
20
|
+
|
21
|
+
current = todo.shift
|
22
|
+
break if current == to
|
23
|
+
|
24
|
+
next if visited[current]
|
25
|
+
visited[current] = true
|
26
|
+
|
27
|
+
next if pool[current].nil?
|
28
|
+
pool[current].each { |node, edge|
|
29
|
+
prev[node] ||= edge
|
30
|
+
todo << node
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
to_node = to
|
35
|
+
route = []
|
36
|
+
while edge = prev[to_node]
|
37
|
+
to_node = edge.first
|
38
|
+
route.unshift edge
|
39
|
+
break if from == to_node
|
40
|
+
end
|
41
|
+
|
42
|
+
route
|
43
|
+
end
|
44
|
+
end
|
data/lib/prick.rb
CHANGED
@@ -1,20 +1,28 @@
|
|
1
|
-
require
|
1
|
+
require "semantic" # Defined here instead of in prick/version.rb to avoid having Gem depend on it
|
2
2
|
|
3
|
-
|
4
|
-
require "
|
3
|
+
# Needs to go first because it contains class-level methods that is used by other modules
|
4
|
+
require "ext/forward_method.rb"
|
5
5
|
|
6
|
-
require "prick/
|
7
|
-
require "prick/
|
6
|
+
require "prick/branch.rb"
|
7
|
+
require "prick/builder.rb"
|
8
|
+
require "prick/migration.rb"
|
8
9
|
require "prick/command.rb"
|
10
|
+
require "prick/constants.rb"
|
9
11
|
require "prick/database.rb"
|
10
|
-
require "prick/
|
11
|
-
require "prick/
|
12
|
+
require "prick/diff.rb"
|
13
|
+
require "prick/exceptions.rb"
|
12
14
|
require "prick/git.rb"
|
13
|
-
require "prick/
|
15
|
+
require "prick/project.rb"
|
14
16
|
require "prick/rdbms.rb"
|
15
17
|
require "prick/schema.rb"
|
18
|
+
require "prick/share.rb"
|
19
|
+
require "prick/state.rb"
|
16
20
|
require "prick/version.rb"
|
17
|
-
|
21
|
+
|
22
|
+
require "ext/fileutils.rb"
|
23
|
+
require "ext/shortest_path.rb"
|
24
|
+
|
25
|
+
require "indented_io"
|
18
26
|
|
19
27
|
module Prick
|
20
28
|
end
|
data/lib/prick/branch.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
require "prick/state.rb"
|
2
|
+
|
3
|
+
module Prick
|
4
|
+
class Branch
|
5
|
+
# Branch name. It is usually equal to the version but migrations use a
|
6
|
+
# <base_version>_<version> format instead
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# Version of this branch. Note that version and base_version are the same
|
10
|
+
# for feature branches
|
11
|
+
attr_reader :version
|
12
|
+
|
13
|
+
# Base version
|
14
|
+
attr_reader :base_version
|
15
|
+
|
16
|
+
# The release directory. It contains the release's .prick-migration file
|
17
|
+
attr_reader :directory
|
18
|
+
|
19
|
+
# The Schema object. This is shared by many all branches
|
20
|
+
attr_reader :schema
|
21
|
+
|
22
|
+
# Migration object. Running the migration object on the base release will
|
23
|
+
# migrate it to this release
|
24
|
+
attr_reader :migration
|
25
|
+
|
26
|
+
# Database name
|
27
|
+
def database() name end
|
28
|
+
|
29
|
+
# Classifiers
|
30
|
+
def release?() self.is_a?(Release) && !prerelease? end
|
31
|
+
def prerelease?() self.is_a?(PreRelease) end
|
32
|
+
def feature?() self.is_a?(Feature) end
|
33
|
+
def migration?() self.is_a?(MigrationRelease) end
|
34
|
+
|
35
|
+
# Note that `name` can be nil. It defaults to `version.to_s`
|
36
|
+
def initialize(name, version, base_version, directory, migration)
|
37
|
+
@name = name || migration.version.to_s
|
38
|
+
@version = version
|
39
|
+
@base_version = base_version
|
40
|
+
@directory = directory
|
41
|
+
@schema = Schema.new
|
42
|
+
@migration = migration
|
43
|
+
end
|
44
|
+
|
45
|
+
def dump
|
46
|
+
puts "#{self.class}"
|
47
|
+
indent {
|
48
|
+
puts "name: #{name}"
|
49
|
+
puts "version: #{version}"
|
50
|
+
puts "base_version: #{base_version}"
|
51
|
+
puts "directory: #{directory}"
|
52
|
+
print "migration: "
|
53
|
+
migration.dump
|
54
|
+
puts "database: #{database}"
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load(name) raise NotThis end
|
59
|
+
|
60
|
+
# True if the branch exists in git
|
61
|
+
def exist?() self.class.exist?(name) end
|
62
|
+
|
63
|
+
def create()
|
64
|
+
!exist? or raise Error, "Can't create branch #{name}, exists already"
|
65
|
+
Git.create_branch(name)
|
66
|
+
Git.checkout_branch(name)
|
67
|
+
prepare if !prepared?
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# True if the branch exists on disk
|
72
|
+
def present?() self.class.present?(name) end
|
73
|
+
|
74
|
+
def prepared?() @migration.exist? end
|
75
|
+
def prepare() @migration.create end
|
76
|
+
|
77
|
+
def include(feature_version) @migration.append_feature(feature_version) end
|
78
|
+
|
79
|
+
def build(database) schema.build(database) end
|
80
|
+
|
81
|
+
# Used to checkout migrations. MigrationReleases checks out the
|
82
|
+
# corresponding branch while Release and PreRelease checks out a tag
|
83
|
+
def checkout_release() Git.checkout_branch(version) end
|
84
|
+
|
85
|
+
def migrate(database)
|
86
|
+
@migration.migrate(database)
|
87
|
+
end
|
88
|
+
|
89
|
+
def migrate_features(database)
|
90
|
+
@migration.migrate_features(database)
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.directory(name) raise NotThis end
|
94
|
+
def self.exist?(name) Git.branch?(name) || Git.tag?(name) end
|
95
|
+
def self.present?(name) File.exist?(directory(name)) end
|
96
|
+
|
97
|
+
def <=>(other)
|
98
|
+
if !self.is_a?(MigrationRelease) && other.is_a?(MigrationRelease)
|
99
|
+
compare(other.base_version, other.version)
|
100
|
+
else
|
101
|
+
compare(other.version, other.base_version)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def compare(other_version, other_base_version)
|
107
|
+
r = version <=> other_version
|
108
|
+
return r if r != 0
|
109
|
+
if base_version.nil?
|
110
|
+
other_base_version.nil? ? 0 : -1
|
111
|
+
elsif other_base_version.nil?
|
112
|
+
1
|
113
|
+
else
|
114
|
+
base_version <=> other_base_version
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class AbstractRelease < Branch
|
120
|
+
def create(schema_version = self.version)
|
121
|
+
super()
|
122
|
+
schema.version = schema_version
|
123
|
+
Git.add schema.version_file
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
def checkout_release() Git.checkout_tag(version) end
|
128
|
+
|
129
|
+
def tag!() Git.create_tag(version) end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Release < AbstractRelease
|
133
|
+
def initialize(version, base_version)
|
134
|
+
!version.zero? || base_version.nil? or raise Internal, "Version 0.0.0 has no base release"
|
135
|
+
directory = self.class.directory(version.to_s)
|
136
|
+
migration = ReleaseMigration.new(version, base_version)
|
137
|
+
super(nil, version, base_version, directory, migration)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.load(name)
|
141
|
+
migration = ReleaseMigration.load(directory(name))
|
142
|
+
self.new(migration.version, migration.base_version)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.directory(name)
|
146
|
+
File.join(RELEASES_DIR, name)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class PreRelease < AbstractRelease
|
151
|
+
attr_reader :prerelease_version
|
152
|
+
|
153
|
+
def database() version.to_s end
|
154
|
+
|
155
|
+
def initialize(prerelease_version, base_version)
|
156
|
+
@prerelease_version = prerelease_version
|
157
|
+
version = prerelease_version.truncate(:pre)
|
158
|
+
directory = Release.directory(version.to_s)
|
159
|
+
migration = ReleaseMigration.new(version, base_version)
|
160
|
+
super(prerelease_version.to_s, version, base_version, directory, migration)
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.load(name, prerelease_version)
|
164
|
+
migration = ReleaseMigration.load(directory(name))
|
165
|
+
self.new(prerelease_version, migration.base_version)
|
166
|
+
end
|
167
|
+
|
168
|
+
def create()
|
169
|
+
super(prerelease_version)
|
170
|
+
end
|
171
|
+
|
172
|
+
def increment
|
173
|
+
PreRelease.new(prerelease_version.increment(:pre), base_version)
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.directory(name)
|
177
|
+
version = Version.new(name)
|
178
|
+
Release.directory(version.truncate(:pre).to_s)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class MigrationRelease < Branch
|
183
|
+
def database() version.to_s end
|
184
|
+
|
185
|
+
def initialize(version, base_version)
|
186
|
+
migration = MigrationMigration.new(version, base_version)
|
187
|
+
super(migration.name, version, base_version, migration.dir, migration)
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.load(name)
|
191
|
+
directory = self.directory(name)
|
192
|
+
migration_state = MigrationState.new(directory).read
|
193
|
+
self.new(migration_state.version, migration_state.base_version)
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.directory(name)
|
197
|
+
File.join(MIGRATIONS_DIR, name)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Feature maintains a migration in the parent release but it is ignored when
|
202
|
+
# including the feature
|
203
|
+
class Feature < Branch
|
204
|
+
attr_reader :feature_name
|
205
|
+
|
206
|
+
def database() base_version.to_s end
|
207
|
+
|
208
|
+
def initialize(feature_name, base_version)
|
209
|
+
version = Version.new(base_version, feature: feature_name)
|
210
|
+
migration = FeatureMigration.new(feature_name, base_version)
|
211
|
+
super(version.to_s, version, base_version, migration.features_dir, migration)
|
212
|
+
@feature_name = feature_name
|
213
|
+
end
|
214
|
+
|
215
|
+
def create
|
216
|
+
super()
|
217
|
+
schema.version = version
|
218
|
+
Git.add schema.version_file
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
def include(feature_version) @migration.insert_feature(feature_version) end
|
223
|
+
|
224
|
+
def rebase(base_version)
|
225
|
+
new_version = Version.new(new_base_version, feature: feature_name)
|
226
|
+
name = new_version.to_s
|
227
|
+
|
228
|
+
Git.create_branch(name)
|
229
|
+
Git.checkout_branch(name)
|
230
|
+
|
231
|
+
symlink = self.directory(name)
|
232
|
+
FileUtils.ln_sr(directory, symlink, force: true)
|
233
|
+
Git.add(symlink)
|
234
|
+
|
235
|
+
migration.base_version = new_base_version
|
236
|
+
migration.save
|
237
|
+
|
238
|
+
schema.version = new_version
|
239
|
+
Git.add ect.schema.version_file
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.load(name)
|
243
|
+
directory = self.directory(name)
|
244
|
+
migration_state = MigrationState.new(directory).read
|
245
|
+
self.new(migration_state.version.feature, migration_state.base_version)
|
246
|
+
end
|
247
|
+
|
248
|
+
def self.directory(name)
|
249
|
+
version = Version.new(name)
|
250
|
+
File.join(Release.directory(version.truncate(:feature).to_s), version.feature.to_s)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "prick/state.rb"
|
2
|
+
|
3
|
+
module Prick
|
4
|
+
# Builder is a procedural object for building schemas and executing
|
5
|
+
# migrations. Builder use resources that can be executables, SQL files, YAML
|
6
|
+
# files, or directories. A resource is identified by a name (eg. 'my-tables')
|
7
|
+
# and is used to match a build file. Build files are looked up in the
|
8
|
+
# following order:
|
9
|
+
#
|
10
|
+
# #{name}, #{name}.* executable
|
11
|
+
# #{name}.yml
|
12
|
+
# #{name}.sql
|
13
|
+
# #{name}/
|
14
|
+
#
|
15
|
+
# When a resource match a directory, the directory can contain a special
|
16
|
+
# default resource ('build' or 'migrate') that takes over the rest of the
|
17
|
+
# build process for that directory. Typically, you'll use the 'build.yml' or
|
18
|
+
# 'migrate.yml' to control the build order of the other resources
|
19
|
+
#
|
20
|
+
# If a directory doesn't contain a build resource, the resources in the
|
21
|
+
# directory are built in alphabetic order
|
22
|
+
#
|
23
|
+
class Builder
|
24
|
+
attr_reader :database
|
25
|
+
attr_reader :directory
|
26
|
+
attr_reader :default # either "build" or "migrate"
|
27
|
+
|
28
|
+
def initialize(database, directory, default)
|
29
|
+
@database = database
|
30
|
+
@directory = directory
|
31
|
+
@default = default
|
32
|
+
end
|
33
|
+
|
34
|
+
def build(subject = nil)
|
35
|
+
# puts "Building #{subject.inspect}"
|
36
|
+
Dir.chdir(directory) {
|
37
|
+
if subject
|
38
|
+
build_subject(subject)
|
39
|
+
else
|
40
|
+
build_directory(".")
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.yml_file(directory) raise NotThis end
|
46
|
+
|
47
|
+
protected
|
48
|
+
def do_dir(path, &block)
|
49
|
+
dir, file = File.split(path)
|
50
|
+
Dir.chdir(dir) { yield(file) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def do_sql(path)
|
54
|
+
# puts "do_sql(#{path})"
|
55
|
+
do_dir(path) { |file| Rdbms.exec_file(database.name, file, user: database.user) }
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def do_exe(path)
|
60
|
+
# puts "do_exe(#{path})"
|
61
|
+
do_dir(path) { |file| Command.command "#{file} #{database.name} #{database.user}" } # FIXME Security
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
def do_yml(path)
|
66
|
+
# puts "do_yml(#{path})"
|
67
|
+
dir, file = File.split(path)
|
68
|
+
YAML.load(File.read(path))&.each { |subject| build_subject(File.join(dir, subject)) }
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
# A subject can be both an abstract subject or a concrete file (*.yml, *.sql)
|
73
|
+
def build_subject(subject)
|
74
|
+
if File.file?(subject) && File.executable?(subject)
|
75
|
+
do_exe(subject)
|
76
|
+
elsif subject.end_with?(".yml")
|
77
|
+
do_yml(subject)
|
78
|
+
elsif subject.end_with?(".sql")
|
79
|
+
do_sql(subject)
|
80
|
+
elsif File.exist?(yml_file = "#{subject}.yml")
|
81
|
+
do_yml(yml_file)
|
82
|
+
elsif File.exist?(sql_file = "#{subject}.sql")
|
83
|
+
do_sql(sql_file)
|
84
|
+
elsif File.directory?(subject)
|
85
|
+
build_directory(subject)
|
86
|
+
else
|
87
|
+
false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_directory(path)
|
92
|
+
Dir.chdir(path) {
|
93
|
+
build_subject(File.join(path, "build")) || begin
|
94
|
+
if Dir["#{path}/#{default}", "#{path}/#{default}.yml", "#{path}/#{default}.sql"]
|
95
|
+
subjects = [default]
|
96
|
+
else
|
97
|
+
exes = candidates.select { |file| File.file?(file) && File.executable?(file) }
|
98
|
+
ymls = candidates.select { |file| file.end_with?(".yml") }.map { |f| f.sub(/\.yml$/, "") }
|
99
|
+
sqls = candidates.select { |file| file.end_with?(".sql") }.map { |f| f.sub(/\.sql$/, "") }
|
100
|
+
dirs = candidates.select { |file| File.directory?(file) }
|
101
|
+
subjects = (exes + ymls + sqls + dirs).uniq.sort.reject { |f| f != "diff" }
|
102
|
+
end
|
103
|
+
subjects.inject(false) { |a, s| build_subject(s) || a }
|
104
|
+
end
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class MigrationBuilder < Builder
|
110
|
+
def initialize(database, directory)
|
111
|
+
super(database, directory, "migrate")
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.yml_file(directory) File.join(directory, "migrate") + ".yml" end
|
115
|
+
end
|
116
|
+
|
117
|
+
class SchemaBuilder < Builder
|
118
|
+
def initialize(database, directory)
|
119
|
+
super(database, directory, "build")
|
120
|
+
end
|
121
|
+
|
122
|
+
def build(subject = nil)
|
123
|
+
if subject
|
124
|
+
Dir.chdir(directory) {
|
125
|
+
if File.executable?(subject)
|
126
|
+
do_exe(subject)
|
127
|
+
elsif subject.end_with?(".sql")
|
128
|
+
do_sql(subject)
|
129
|
+
else
|
130
|
+
build_subject(subject)
|
131
|
+
end
|
132
|
+
}
|
133
|
+
else
|
134
|
+
super()
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.yml_file(directory) File.join(directory, "build") + ".yml" end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|