prick 0.2.0 → 0.7.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 (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
+