prick 0.2.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -5
  3. data/Gemfile +4 -1
  4. data/TODO +10 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +328 -402
  7. data/lib/ext/fileutils.rb +18 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +20 -10
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +164 -0
  13. data/lib/prick/cache.rb +34 -0
  14. data/lib/prick/command.rb +19 -11
  15. data/lib/prick/constants.rb +122 -48
  16. data/lib/prick/database.rb +28 -20
  17. data/lib/prick/diff.rb +125 -0
  18. data/lib/prick/exceptions.rb +15 -3
  19. data/lib/prick/git.rb +77 -30
  20. data/lib/prick/head.rb +183 -0
  21. data/lib/prick/migration.rb +40 -200
  22. data/lib/prick/program.rb +493 -0
  23. data/lib/prick/project.rb +523 -351
  24. data/lib/prick/rdbms.rb +4 -13
  25. data/lib/prick/schema.rb +16 -90
  26. data/lib/prick/share.rb +64 -0
  27. data/lib/prick/state.rb +192 -0
  28. data/lib/prick/version.rb +62 -29
  29. data/libexec/strip-comments +33 -0
  30. data/make_releases +48 -345
  31. data/make_schema +10 -0
  32. data/prick.gemspec +14 -23
  33. data/share/diff/diff.after-tables.sql +4 -0
  34. data/share/diff/diff.before-tables.sql +4 -0
  35. data/share/diff/diff.tables.sql +8 -0
  36. data/share/migration/diff.tables.sql +8 -0
  37. data/share/migration/features.yml +6 -0
  38. data/share/migration/migrate.sql +3 -0
  39. data/share/migration/migrate.yml +8 -0
  40. data/share/migration/tables.sql +3 -0
  41. data/share/schema/build.yml +14 -0
  42. data/share/schema/schema.sql +5 -0
  43. data/share/schema/schema/build.yml +3 -0
  44. data/share/schema/schema/prick/build.yml +14 -0
  45. data/share/schema/schema/prick/data.sql +7 -0
  46. data/share/schema/schema/prick/schema.sql +5 -0
  47. data/share/{schemas/prick/schema.sql → schema/schema/prick/tables.sql} +2 -5
  48. data/{file → share/schema/schema/public/.keep} +0 -0
  49. data/share/schema/schema/public/build.yml +14 -0
  50. data/share/schema/schema/public/schema.sql +3 -0
  51. data/test_assorted +192 -0
  52. data/test_feature +112 -0
  53. data/test_refactor +34 -0
  54. data/test_single_dev +83 -0
  55. metadata +43 -68
  56. data/lib/prick/build.rb +0 -376
  57. data/lib/prick/migra.rb +0 -22
  58. data/share/schemas/prick/data.sql +0 -8
data/lib/ext/fileutils.rb CHANGED
@@ -1,8 +1,26 @@
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)
10
+ file
11
+ end
12
+
13
+ def self.ln_sr(from, to)
14
+ from = Pathname.new(from)
15
+ to_dir = File.dirname(to)
16
+ to_file = File.basename(to)
17
+ relpath = from.relative_path_from(File.dirname(to))
18
+ Dir.chdir(to_dir) { FileUtils.ln_s(relpath, File.basename(to)) }
19
+ end
20
+
21
+ def self.empty!(dir)
22
+ Dir.chdir(dir) {
23
+ FileUtils.rm_rf(Dir.children("."))
24
+ }
7
25
  end
8
26
  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,21 +1,31 @@
1
- require 'semantic' # https://github.com/jlindsey/semantic
2
1
 
3
- require "prick/constants.rb"
4
- require "prick/exceptions.rb"
2
+ $LOAD_PATH.unshift("/home/clr/prj/shellopts/lib")
3
+
4
+ # 'semantic' is required here instead of in prick/version.rb to avoid having Gem depend on it
5
+ require "semantic"
5
6
 
6
- require "prick/archive.rb"
7
- require "prick/build.rb"
7
+ # Needs to go first because it contains class-level methods that is used by other modules
8
+ require "ext/forward_method.rb"
9
+
10
+ require "prick/builder.rb"
11
+ require "prick/migration.rb"
12
+ require "prick/cache.rb"
8
13
  require "prick/command.rb"
14
+ require "prick/constants.rb"
9
15
  require "prick/database.rb"
10
- require "prick/dsort.rb"
11
- require "prick/ensure.rb"
16
+ require "prick/diff.rb"
17
+ require "prick/exceptions.rb"
12
18
  require "prick/git.rb"
13
- require "prick/migra.rb"
19
+ require "prick/head.rb"
20
+ require "prick/project.rb"
14
21
  require "prick/rdbms.rb"
15
22
  require "prick/schema.rb"
23
+ require "prick/share.rb"
24
+ require "prick/state.rb"
16
25
  require "prick/version.rb"
17
- require "prick/project.rb"
26
+
27
+ require "ext/fileutils.rb"
28
+ require "ext/shortest_path.rb"
18
29
 
19
30
  module Prick
20
31
  end
21
-
@@ -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,164 @@
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
+ attr_reader :lines
28
+
29
+ def initialize(database, directory, default)
30
+ @database = database
31
+ @directory = directory
32
+ @default = default
33
+ @execute = true
34
+ @lines = []
35
+ end
36
+
37
+ def build(subject = default, execute: true)
38
+ @execute = execute
39
+ @lines = []
40
+ # puts "Building #{subject.inspect}"
41
+ Dir.chdir(directory) {
42
+ if subject
43
+ build_subject(subject)
44
+ else
45
+ build_directory(".")
46
+ end
47
+ }
48
+ @lines.reject! { |l| l =~ /^\s*--/ || l =~ /^\s*$/ }
49
+ self
50
+ end
51
+
52
+ def self.yml_file(directory) raise NotThis end
53
+
54
+ protected
55
+ def do_dir(path, &block)
56
+ dir, file = File.split(path)
57
+ Dir.chdir(dir) { yield(file) }
58
+ end
59
+
60
+ def do_sql(path)
61
+ # puts "do_sql(#{path})"
62
+ do_dir(path) { |file|
63
+ if @execute
64
+ Rdbms.exec_file(database.name, file, user: database.user)
65
+ else
66
+ @lines += IO.readlines(file).map(&:chomp)
67
+ end
68
+ }
69
+ true
70
+ end
71
+
72
+ def do_exe(path)
73
+ # puts "do_exe(#{path})"
74
+ do_dir(path) { |file|
75
+ lines = Command.command "#{file} #{database.name} #{database.user}" # FIXME Security
76
+ if @execute
77
+ Rdbms.exec_sql(database.name, lines.join("\n"), database.user)
78
+ else
79
+ @lines += lines
80
+ end
81
+ }
82
+ true
83
+ end
84
+
85
+ def do_yml(path)
86
+ # puts "do_yml(#{path})"
87
+ dir, file = File.split(path)
88
+ YAML.load(File.read(path))&.each { |subject| build_subject(File.join(dir, subject)) }
89
+ true
90
+ end
91
+
92
+ # A subject can be both an abstract subject or a concrete file (*.yml, *.sql)
93
+ def build_subject(subject)
94
+ if File.file?(subject) && File.executable?(subject)
95
+ do_exe(subject)
96
+ elsif File.file?(subject) && subject.end_with?(".yml")
97
+ do_yml(subject)
98
+ elsif File.file?(subject) && subject.end_with?(".sql")
99
+ do_sql(subject)
100
+ elsif File.exist?(yml_file = "#{subject}.yml")
101
+ do_yml(yml_file)
102
+ elsif File.exist?(sql_file = "#{subject}.sql")
103
+ do_sql(sql_file)
104
+ elsif File.directory?(subject)
105
+ build_directory(subject)
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ def build_directory(path)
112
+ Dir.chdir(path) {
113
+ build_subject(File.join(path, @default)) || begin
114
+ if Dir["#{path}/#{default}", "#{path}/#{default}.yml", "#{path}/#{default}.sql"]
115
+ subjects = [default]
116
+ else
117
+ exes = candidates.select { |file| File.file?(file) && File.executable?(file) }
118
+ ymls = candidates.select { |file| file.end_with?(".yml") }.map { |f| f.sub(/\.yml$/, "") }
119
+ sqls = candidates.select { |file| file.end_with?(".sql") }.map { |f| f.sub(/\.sql$/, "") }
120
+ dirs = candidates.select { |file| File.directory?(file) }
121
+ subjects = (exes + ymls + sqls + dirs).uniq.sort.reject { |f| f != "diff" }
122
+ end
123
+ subjects.inject(false) { |a, s| build_subject(s) || a }
124
+ end
125
+ }
126
+ end
127
+ end
128
+
129
+ class MigrationBuilder < Builder
130
+ def initialize(database, directory)
131
+ super(database, directory, "migrate")
132
+ end
133
+
134
+ def self.yml_file(directory) File.join(directory, "migrate") + ".yml" end
135
+ end
136
+
137
+ class SchemaBuilder < Builder
138
+ def initialize(database, directory)
139
+ super(database, directory, "build")
140
+ end
141
+
142
+ def build(subject = nil, execute: true)
143
+ if subject
144
+ @execute = execute
145
+ @lines = []
146
+ Dir.chdir(directory) {
147
+ if File.executable?(subject)
148
+ do_exe(subject)
149
+ elsif subject.end_with?(".sql")
150
+ do_sql(subject)
151
+ else
152
+ build_subject(subject)
153
+ end
154
+ }
155
+ else
156
+ super(execute: execute)
157
+ end
158
+ self
159
+ end
160
+
161
+ def self.yml_file(directory) File.join(directory, "build") + ".yml" end
162
+ end
163
+ end
164
+