prick 0.3.0 → 0.8.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/TODO +7 -0
  4. data/exe/prick +235 -163
  5. data/lib/ext/fileutils.rb +7 -0
  6. data/lib/prick.rb +7 -5
  7. data/lib/prick/builder.rb +31 -8
  8. data/lib/prick/cache.rb +34 -0
  9. data/lib/prick/constants.rb +106 -54
  10. data/lib/prick/database.rb +26 -20
  11. data/lib/prick/diff.rb +103 -25
  12. data/lib/prick/git.rb +31 -9
  13. data/lib/prick/head.rb +183 -0
  14. data/lib/prick/migration.rb +41 -181
  15. data/lib/prick/program.rb +255 -0
  16. data/lib/prick/project.rb +264 -0
  17. data/lib/prick/rdbms.rb +3 -12
  18. data/lib/prick/schema.rb +6 -11
  19. data/lib/prick/state.rb +129 -74
  20. data/lib/prick/version.rb +41 -28
  21. data/prick.gemspec +3 -1
  22. data/share/diff/diff.after-tables.sql +4 -0
  23. data/share/diff/diff.before-tables.sql +4 -0
  24. data/share/diff/diff.tables.sql +8 -0
  25. data/share/migration/diff.tables.sql +8 -0
  26. data/share/{release_migration → migration}/features.yml +0 -0
  27. data/share/migration/migrate.sql +3 -0
  28. data/share/{release_migration → migration}/migrate.yml +3 -0
  29. data/share/migration/tables.sql +3 -0
  30. data/share/{schemas → schema/schema}/build.yml +0 -0
  31. data/share/{schemas → schema/schema}/prick/build.yml +0 -0
  32. data/share/schema/schema/prick/data.sql +7 -0
  33. data/share/{schemas → schema/schema}/prick/schema.sql +0 -0
  34. data/share/{schemas → schema/schema}/prick/tables.sql +2 -2
  35. data/share/{schemas → schema/schema}/public/.keep +0 -0
  36. data/share/{schemas → schema/schema}/public/build.yml +0 -0
  37. data/share/{schemas → schema/schema}/public/schema.sql +0 -0
  38. data/test_refactor +34 -0
  39. metadata +23 -21
  40. data/file +0 -0
  41. data/lib/prick/build.rb +0 -376
  42. data/lib/prick/migra.rb +0 -22
  43. data/share/feature_migration/diff.sql +0 -2
  44. data/share/feature_migration/migrate.sql +0 -2
  45. data/share/release_migration/diff.sql +0 -3
  46. data/share/release_migration/migrate.sql +0 -5
  47. data/share/schemas/prick/data.sql +0 -7
data/lib/prick/program.rb CHANGED
@@ -3,6 +3,261 @@ require "prick.rb"
3
3
 
4
4
  module Prick
5
5
  # Implements the command line commands
6
+ class Program
7
+ # Lazy-constructed because Project can only be initialized when the
8
+ # directory structure is present
9
+ def project() @project ||= Project.load end
10
+
11
+ attr_accessor :quiet
12
+ attr_accessor :verbose
13
+
14
+ def initialize(quiet: false, verbose: false)
15
+ @quiet = quiet
16
+ @verbose = verbose
17
+ end
18
+
19
+ # Check if the git repository is clean. Raise an error if not
20
+ def check_clean()
21
+ Git.clean? or raise Error, "Repository is dirty - please commit your changes first"
22
+ end
23
+
24
+ # Create project directory structure
25
+ def init(name, user, directory)
26
+ !Project.exist?(directory) or raise Error, "Directory #{directory} is already initialized"
27
+ Project.create(name, user, directory)
28
+ if name != directory
29
+ mesg "Initialized project #{name} in #{directory}"
30
+ else
31
+ mesg "Initialized project #{name}"
32
+ end
33
+ end
34
+
35
+ def info
36
+ if project.tag?
37
+ puts "At v#{project.version} tag"
38
+ else
39
+ puts "On branch #{project.head.name}, #{project.version}"
40
+ end
41
+ puts " Git is " + (Git.clean? ? "clean" : "dirty")
42
+ bv = project.head.version
43
+ dv = project.database.version
44
+ sv = project.head.schema.version
45
+ puts " Database version: #{dv}" + (dv != bv ? " (mismatch)" : "")
46
+ puts " Schema version : #{sv}" + (sv != bv ? " (mismatch)" : "")
47
+ end
48
+
49
+ def list_releases(migrations: false, cancelled: false)
50
+ raise NotYet
51
+ end
52
+
53
+ def list_migrations
54
+ raise NotYet
55
+ end
56
+
57
+ def list_upgrades(from = nil, to = nil)
58
+ raise NotYet
59
+ end
60
+
61
+ def list_cache
62
+ project.cache.list.each { |l| puts l }
63
+ end
64
+
65
+ def build(database, version, nocache)
66
+ into_mesg = database && "into #{database}"
67
+ version_mesg = version ? "v#{version}" : "current schema"
68
+ version &&= Version.new(version)
69
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
70
+ database = database ? Database.new(database, project.user) : project.database(version)
71
+ project.build(database, version: version)
72
+ project.save(database) if version && !nocache
73
+ mesg "Built", version_mesg, into_mesg
74
+ end
75
+
76
+ def make(database, version, nocache)
77
+ version &&= Version.new(version)
78
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
79
+ if !nocache && version && project.cache.exist?(version)
80
+ load(database, version)
81
+ else
82
+ build(database, version, nocache)
83
+ end
84
+ end
85
+
86
+ def make_clean(all)
87
+ project.cache.clean.each { |file| mesg "Removed cache file #{File.basename(file)}" }
88
+ drop(nil, all)
89
+ end
90
+
91
+ def load(database, file_or_version)
92
+ check_owned(database) if database
93
+ into_mesg = database && "into #{database}"
94
+ if version = Version.try(file_or_version)
95
+ database = database ? Database.new(database, project.user) : project.database(version)
96
+ project.load(database, version: version)
97
+ mesg "Loaded v#{version}", into_mesg, "from cache"
98
+ else file = file_or_version
99
+ project.load(database, file: file)
100
+ mesg "Loaded #{File.basename(file)}", into_mesg
101
+ end
102
+ end
103
+
104
+ def save(version, file)
105
+ database = project.database(version)
106
+ database.exist? or raise "Can't find database '#{database}'"
107
+ subj_mesg = file ? file : "cache"
108
+ project.save(database, file: file)
109
+ mesg "Saved #{database} to #{subj_mesg}"
110
+ end
111
+
112
+ def drop(database, all)
113
+ database.nil? || !all or raise Error, "Can't use --all when database is given"
114
+ if database
115
+ check_owned(database)
116
+ dbs = [database]
117
+ else
118
+ dbs = Rdbms.list_databases(Prick.database_re(project.name))
119
+ dbs << project.name if all && project.database.exist?
120
+ end
121
+ dbs += Rdbms.list_databases(Prick.tmp_databases_re(project.name)) # FIXME: Only used in dev
122
+ dbs.each { |db|
123
+ Rdbms.drop_database(db)
124
+ mesg "Dropped database #{db}"
125
+ }
126
+ end
127
+
128
+ # `select` is a tri-state variable that can be :tables, :no_tables, or :all
129
+ # (or any other value)
130
+ def diff(from, to, mark, select)
131
+ diff = project.diff(from && Version.new(from), to && Version.new(to))
132
+ if !diff.same?
133
+ if select != :tables && !diff.before_table_changes.empty?
134
+ puts "-- BEFORE TABLE CHANGES" if mark
135
+ puts diff.before_table_changes
136
+ end
137
+ if select != :no_tables && !diff.table_changes.empty?
138
+ puts "-- TABLE CHANGES" if mark
139
+ puts diff.table_changes
140
+ end
141
+ if select != :tables && !diff.after_table_changes.empty?
142
+ puts "-- AFTER TABLE CHANGES" if mark
143
+ puts diff.after_table_changes
144
+ end
145
+ end
146
+ end
147
+
148
+ def migrate
149
+ raise NotYet
150
+ end
151
+
152
+ def prepare_release(fork)
153
+ project.prepare_release(fork)
154
+ end
155
+
156
+ def prepare_feature(name)
157
+ raise NotYet
158
+ end
159
+
160
+ def prepare_migration(from = nil)
161
+ raise NotYet
162
+ end
163
+
164
+ def prepare_schema(name)
165
+ raise NotYet
166
+ end
167
+
168
+ def prepare_diff(version = nil)
169
+ # Helpful check to ensure the user has built the current version
170
+ # Diff.same?(to_db, database) or
171
+ # raise Error, "Schema and project database are not synchronized"
172
+
173
+ project.prepare_diff(version || project.version)
174
+ if FileUtils.compare_file(TABLES_DIFF_PATH, TABLES_DIFF_SHARE_PATH)
175
+ mesg "Created diff. No table changes found, no manual migration updates needed"
176
+ else
177
+ mesg "Created diff. Please inspect #{TABLES_DIFF_PATH} and incorporate the"
178
+ mesg "changes in #{MIGRATION_DIR}/migrate.sql"
179
+ end
180
+ end
181
+
182
+ def include(name)
183
+ raise NotYet
184
+ end
185
+
186
+ def check
187
+ version ||=
188
+ if project.prerelease? || project.migration?
189
+ project.branch.base_version
190
+ else
191
+ project.branch.version
192
+ end
193
+ project.check_migration(version)
194
+ end
195
+
196
+ def create_release(version = nil)
197
+ if project.prerelease_branch?
198
+ raise NotYet
199
+ elsif project.release_branch?
200
+ project.create_release(Version.new(version))
201
+ else
202
+ raise Error, "You need to be on a release or pre-release branch to create a new release"
203
+ end
204
+ mesg "Created release v#{project.head.version}"
205
+ end
206
+
207
+ def create_prerelease(version = nil)
208
+ raise NotYet
209
+ end
210
+
211
+ def create_feature(name)
212
+ raise NotYet
213
+ end
214
+
215
+ def cancel(version)
216
+ raise NotYet
217
+ end
218
+
219
+ def generate_schema
220
+ project.generate_schema
221
+ end
222
+
223
+ def generate_migration
224
+ project.generate_migration
225
+ end
226
+
227
+ def upgrade
228
+ raise NotYet
229
+ end
230
+
231
+ # TODO: Create a Backup class and a Project#backup_store object
232
+ def backup(file = nil)
233
+ file = file || File.join(SPOOL_DIR, "#{project.name}-#{Time.now.utc.strftime("%Y%m%d-%H%M%S")}.sql.gz")
234
+ project.save(file: file)
235
+ mesg "Backed-up database to #{file}"
236
+ end
237
+
238
+ def restore(file_arg = nil)
239
+ file = file_arg || Dir.glob(File.join(SPOOL_DIR, "#{name}-*.sql.gz")).sort.last
240
+ File.exist?(file) or raise Error, "Can't find #{file_arg || "any backup file"}"
241
+ project.load(file: file)
242
+ mesg "Restored database from #{file}"
243
+ end
244
+
245
+ protected
246
+ def check_owned(database)
247
+ database =~ ALL_DATABASES_RE or raise Error, "Not a prick database: #{database}"
248
+ project_name = $1
249
+ project_name == project.name or raise Error, "Database not owned by this prick project: #{database}"
250
+ end
251
+
252
+ def mesg(*args) puts args.compact.grep(/\S/).join(' ') if !quiet end
253
+ def verb(*args) puts args.compact.grep(/\S/).join(' ') if verbose end
254
+ end
255
+ end
256
+
257
+ __END__
258
+
259
+
260
+ module Prick
6
261
  class Program
7
262
  def project() @project ||= Project.load end
8
263
 
data/lib/prick/project.rb CHANGED
@@ -2,6 +2,270 @@ require "prick/state.rb"
2
2
 
3
3
  require "tmpdir"
4
4
 
5
+ module Prick
6
+ class Project
7
+ attr_reader :name
8
+ attr_reader :user # Database user
9
+ attr_reader :head # Current branch/tag
10
+ attr_reader :schema
11
+
12
+ # Return the versioned database or the project database if `version` is nil
13
+ def database(version = nil)
14
+ version ? Database.new("#{name}-#{version}", user) : @database
15
+ end
16
+
17
+ attr_reader :cache
18
+
19
+ def initialize(name, user, head)
20
+ @name = name
21
+ @user = user || ENV['USER']
22
+ @head = head
23
+ @schema = Schema.new
24
+ @database = Database.new(name, user)
25
+ @cache = Cache.new
26
+ end
27
+
28
+ forward_methods :version, :base_version, :@head
29
+ forward_methods :clean?, :@head
30
+ forward_methods :tag?, :release_tag?, :migration_tag?, :@head
31
+ forward_methods :branch?, :release_branch?, :prerelease_branch?, :feature_branch?, :migration_branch?, :@head
32
+
33
+ def self.exist?(directory)
34
+ File.directory?(directory) && Dir.chdir(directory) { ProjectState.new.exist? }
35
+ end
36
+
37
+ def self.create(name, user, directory)
38
+ user ||= ENV['USER']
39
+
40
+ FileUtils.mkdir(directory) if directory != "."
41
+
42
+ Dir.chdir(directory) {
43
+ # Initialize git instance
44
+ Git.init
45
+
46
+ # Create prick version file
47
+ PrickVersion.new.write(VERSION)
48
+
49
+ # Create project state file
50
+ ProjectState.new(name: name, user: user).write
51
+
52
+ # Directories
53
+ FileUtils.mkdir_p(DIRS)
54
+ DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
55
+
56
+ # Copy default gitignore and schema files
57
+ Share.cp("gitignore", ".gitignore")
58
+ Share.cp("schema/schema", ".")
59
+
60
+ # Add .prick-migration file
61
+ MigrationState.new(version: Version.zero).create
62
+
63
+ # Add everything so far
64
+ Git.add(".")
65
+ Git.commit("Initial import")
66
+
67
+ # Rename branch "master"/"main" to "0.0.0_initial"
68
+ from_branch = Git.branch?("master") ? "master" : "main"
69
+ Git.rename_branch(from_branch, "0.0.0_initial")
70
+
71
+ # Create schema
72
+ schema = Schema.new
73
+ schema.version = Version.zero
74
+ Git.add schema.version_file
75
+
76
+ # Create release
77
+ Git.commit "Created release v0.0.0"
78
+ Git.create_tag(Version.zero)
79
+ Git.checkout_tag(Version.zero)
80
+ }
81
+ end
82
+
83
+ # Initialize from disk
84
+ #
85
+ # TODO Handle migrations
86
+ def self.load
87
+ if Git.detached?
88
+ name = "v#{Git.current_tag}"
89
+ else
90
+ name = Git.current_branch
91
+ end
92
+ begin
93
+ branch = Head.load(name)
94
+ rescue Version::FormatError
95
+ raise Fail, "Illegal branch name: #{name}"
96
+ end
97
+ state = ProjectState.new.read
98
+ Project.new(state.name, state.user, branch)
99
+ end
100
+
101
+ def build(database = self.database, version: nil)
102
+ database.clean
103
+ if version
104
+ FileUtils.mkdir_p(TMP_DIR)
105
+ Dir.mktmpdir("clone-", TMP_DIR) { |dir|
106
+ Command.command "git clone . #{dir}"
107
+ Dir.chdir(dir) {
108
+ Git.checkout_tag(version)
109
+ project = Project.load
110
+ project.head.build(database)
111
+ }
112
+ }
113
+ else
114
+ head.build(database)
115
+ end
116
+ self
117
+ end
118
+
119
+ def load(database = self.database, version: nil, file: nil)
120
+ version.nil? ^ file.nil? or raise Internal, "Need exactly one of :file and :version to be defined"
121
+ database.clean
122
+ if version
123
+ cache.exist?(version) or raise Internal, "Can't find cache file for database #{database}"
124
+ cache.load(database, version)
125
+ else
126
+ database.load(file)
127
+ end
128
+ self
129
+ end
130
+
131
+ def save(database = nil, file: nil)
132
+ !database.nil? || file or raise Internal, "Need a database when saving to file"
133
+ database ||= self.database
134
+ if file
135
+ database.save(file)
136
+ else
137
+ cache.save(database)
138
+ end
139
+ self
140
+ end
141
+
142
+ # Create a schema diff between two database versions. to_version defaults to the
143
+ # current version. Returns the Diff object
144
+ def diff(from_version, to_version)
145
+ begin
146
+ from_db = Database.new("#{name}-base", user)
147
+ to_db = Database.new("#{name}-next", user)
148
+ from_version ||= version
149
+ build(from_db, version: from_version)
150
+ build(to_db, version: to_version)
151
+ Diff.new(from_db, to_db)
152
+ ensure
153
+ from_db&.drop
154
+ to_db&.drop
155
+ end
156
+ end
157
+
158
+ def prepare_release(fork = nil)
159
+ check_clean(:release_tag)
160
+ @head = ReleaseBranch.new(fork, version).create
161
+ submit "Prepared new release based on v#{version}", true
162
+ self
163
+ end
164
+
165
+ def prepare_diff(from_version = version)
166
+ begin
167
+ from_name = "#{name}-base"
168
+ from_db = Database.new(from_name, user)
169
+ build(from_db, version: from_version)
170
+
171
+ to_name = "#{name}-next"
172
+ to_db = Database.new(to_name, user)
173
+ build(to_db)
174
+
175
+ if prerelease_branch?
176
+ head.migrate_features(from_db)
177
+ end
178
+
179
+ diff = Diff.new(from_db, to_db)
180
+ for path, lines, tables in [
181
+ [BEFORE_TABLES_DIFF_PATH, diff.before_table_changes, false],
182
+ [TABLES_DIFF_PATH, diff.table_changes, true],
183
+ [AFTER_TABLES_DIFF_PATH, diff.after_table_changes, false]]
184
+ if lines.empty?
185
+ if File.exist?(path)
186
+ if tables
187
+ Share.cp(File.join("diff", File.basename(path)), path)
188
+ Git.add(path)
189
+ else
190
+ Git.rm(path)
191
+ end
192
+ end
193
+ else
194
+ Share.cp(File.join("diff", File.basename(path)), path)
195
+ File.open(path, "a") { |f| f.puts lines }
196
+ Git.add(path) if !tables
197
+ end
198
+ end
199
+ self
200
+ ensure
201
+ from_db&.drop
202
+ to_db&.drop
203
+ end
204
+ end
205
+
206
+ def create_release(new_version)
207
+ check_clean(:release_branch)
208
+ head = ReleaseTag.new(new_version, version)
209
+ check_migration(head.migration)
210
+ (@head = head).create
211
+ self
212
+ end
213
+
214
+ def generate_schema
215
+ build = SchemaBuilder.new(database, SCHEMA_DIR).build(execute: false)
216
+ puts build.lines
217
+ end
218
+
219
+ def generate_migration
220
+ build = MigrationBuilder.new(database, MIGRATION_DIR).build(execute: false)
221
+ puts build.lines
222
+ end
223
+
224
+ private
225
+ def check(kind) self.send(:"#{kind}?") end
226
+ def check_clean(kind = nil)
227
+ clean? or raise Internal, "Dirty repository"
228
+ kind.nil? || check(kind) or raise Internal, "Not on a #{kind} tag/branh"
229
+ end
230
+
231
+ def check_branch() branch? or raise Internal, "Not on a branch" end
232
+ def check_tag() tag? or raise Internal, "Not on a tag" end
233
+
234
+ # FIXME: Use Cache::file
235
+ def cache_file(version) File.join(CACHE_DIR, "#{database(version)}.sql.gz") end
236
+
237
+ def submit(msg, commit = true)
238
+ Git.commit msg if commit
239
+ @message = msg
240
+ end
241
+
242
+ def clean(database) # FIXME: Use Database#clean
243
+ if database.exist?
244
+ database.recreate if database.loaded?
245
+ else
246
+ database.create
247
+ end
248
+ end
249
+
250
+ def check_migration(migration)
251
+ begin
252
+ from_version = migration.base_version
253
+ from_db = Database.new("#{name}-base", user)
254
+ to_db = Database.new("#{name}-next", user)
255
+ build(from_db, version: from_version)
256
+ build(to_db)
257
+ migration.migrate(from_db)
258
+ Diff.new(from_db, to_db).same? or raise Error, "Schema/migration mismatch"
259
+ ensure
260
+ from_db&.drop
261
+ to_db&.drop
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ __END__
268
+
5
269
  module Prick
6
270
  class Project
7
271
  # Name of project. Persisted in the project state file