prick 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
+