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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -5
  3. data/Gemfile +4 -1
  4. data/TODO +3 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +224 -370
  7. data/lib/ext/fileutils.rb +11 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +17 -9
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +141 -0
  13. data/lib/prick/command.rb +19 -11
  14. data/lib/prick/constants.rb +42 -20
  15. data/lib/prick/database.rb +5 -3
  16. data/lib/prick/diff.rb +47 -0
  17. data/lib/prick/exceptions.rb +15 -3
  18. data/lib/prick/git.rb +46 -21
  19. data/lib/prick/migration.rb +165 -185
  20. data/lib/prick/program.rb +238 -0
  21. data/lib/prick/project.rb +266 -358
  22. data/lib/prick/rdbms.rb +2 -2
  23. data/lib/prick/schema.rb +19 -88
  24. data/lib/prick/share.rb +64 -0
  25. data/lib/prick/state.rb +137 -0
  26. data/lib/prick/version.rb +34 -14
  27. data/libexec/strip-comments +33 -0
  28. data/make_releases +48 -345
  29. data/make_schema +10 -0
  30. data/prick.gemspec +11 -22
  31. data/share/feature_migration/diff.sql +2 -0
  32. data/share/feature_migration/migrate.sql +2 -0
  33. data/share/release_migration/diff.sql +3 -0
  34. data/share/release_migration/features.yml +6 -0
  35. data/share/release_migration/migrate.sql +5 -0
  36. data/share/release_migration/migrate.yml +5 -0
  37. data/share/schema/build.yml +14 -0
  38. data/share/schema/schema.sql +5 -0
  39. data/share/schemas/build.yml +3 -0
  40. data/share/schemas/prick/build.yml +14 -0
  41. data/share/schemas/prick/data.sql +1 -2
  42. data/share/schemas/prick/schema.sql +0 -15
  43. data/share/schemas/prick/tables.sql +17 -0
  44. data/share/schemas/public/.keep +0 -0
  45. data/share/schemas/public/build.yml +14 -0
  46. data/share/schemas/public/schema.sql +3 -0
  47. data/test_assorted +192 -0
  48. data/test_feature +112 -0
  49. data/test_single_dev +83 -0
  50. metadata +34 -61
@@ -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
@@ -1,20 +1,28 @@
1
- require 'semantic' # https://github.com/jlindsey/semantic
1
+ require "semantic" # Defined here instead of in prick/version.rb to avoid having Gem depend on it
2
2
 
3
- require "prick/constants.rb"
4
- require "prick/exceptions.rb"
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/archive.rb"
7
- require "prick/build.rb"
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/dsort.rb"
11
- require "prick/ensure.rb"
12
+ require "prick/diff.rb"
13
+ require "prick/exceptions.rb"
12
14
  require "prick/git.rb"
13
- require "prick/migra.rb"
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
- require "prick/project.rb"
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
@@ -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
+