prick 0.4.0 → 0.5.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/TODO +7 -0
  4. data/exe/prick +95 -33
  5. data/lib/ext/fileutils.rb +7 -0
  6. data/lib/prick.rb +5 -3
  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 -18
  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 +199 -0
  16. data/lib/prick/project.rb +277 -0
  17. data/lib/prick/rdbms.rb +2 -1
  18. data/lib/prick/schema.rb +5 -10
  19. data/lib/prick/state.rb +129 -74
  20. data/lib/prick/version.rb +41 -28
  21. data/share/diff/diff.after-tables.sql +4 -0
  22. data/share/diff/diff.before-tables.sql +4 -0
  23. data/share/diff/diff.tables.sql +8 -0
  24. data/share/migration/diff.tables.sql +8 -0
  25. data/share/{release_migration → migration}/features.yml +0 -0
  26. data/share/migration/migrate.sql +3 -0
  27. data/share/{release_migration → migration}/migrate.yml +3 -0
  28. data/share/migration/tables.sql +3 -0
  29. data/share/{schemas → schema/schema}/build.yml +0 -0
  30. data/share/{schemas → schema/schema}/prick/build.yml +0 -0
  31. data/share/schema/schema/prick/data.sql +7 -0
  32. data/share/{schemas → schema/schema}/prick/schema.sql +0 -0
  33. data/share/{schemas → schema/schema}/prick/tables.sql +2 -2
  34. data/share/{schemas → schema/schema}/public/.keep +0 -0
  35. data/share/{schemas → schema/schema}/public/build.yml +0 -0
  36. data/share/{schemas → schema/schema}/public/schema.sql +0 -0
  37. data/test_refactor +34 -0
  38. metadata +22 -20
  39. data/file +0 -0
  40. data/lib/prick/build.rb +0 -376
  41. data/lib/prick/migra.rb +0 -22
  42. data/share/feature_migration/diff.sql +0 -2
  43. data/share/feature_migration/migrate.sql +0 -2
  44. data/share/release_migration/diff.sql +0 -3
  45. data/share/release_migration/migrate.sql +0 -5
  46. data/share/schemas/prick/data.sql +0 -7
@@ -1,210 +1,70 @@
1
1
 
2
2
  module Prick
3
- # A Migration consists of a .prick-features file, a .prick-migration file, and a
4
- # set of SQL migration files. Features can be include as subdirectories
5
- #
6
3
  class Migration
7
- # Directory of the .prick_migration file. It is nil in CurrentRelease
8
- # objects that supports the single-developer workflow
9
- def migration_dir() @migration_state && File.dirname(@migration_state.path) end
4
+ attr_reader :version
5
+ attr_reader :base_version
10
6
 
11
- # Directory of the .prick_features file. nil for the initial 0.0.0
12
- # migration. Note that the feature and the migration directories can be the
13
- # same. The features directory also contains the auto-generated files
14
- # diff.sql, features.sql, features.yml, and the user-defined migrations.sql file
15
- def features_dir() @features_state && File.dirname(@features_state.path) end
16
-
17
- # Directory of the migration. Alias for #features_dir. It should be used in
18
- # derived classes when the features and migration directory are the same
19
- def dir() features_dir end
20
-
21
- # Migration file
22
- def migrations_file() File.join(dir, MIGRATIONS_FILE) end
23
-
24
- # Version of the result of running this migration. `version` is nil for the
25
- # current release and features that are not part of a pre-release. Needs to
26
- # be redefined in subclasses where migration_dir is nil
27
- forward_method :version, :version=, :@migration_state
28
-
29
- # Base version of the migration. `base_version` is nil for the initial
30
- # 0.0.0 migration. Needs to be redefined in subclasses where migration_dir
31
- # is nil
32
- forward_method :base_version, :base_version=, :@migration_state
33
-
34
- # List of features in this migration. The features ordered in the same
35
- # order as they're included
36
- forward_method :features, :@features_state
37
-
38
- # Note that migration_dir can be nil
39
- def initialize(migration_dir, features_dir)
40
- @migration_state = migration_dir && MigrationState.new(migration_dir)
41
- @features_state = features_dir && FeaturesState.new(features_dir)
7
+ def initialize(version, base_version)
8
+ @version = version
9
+ @base_version = base_version
42
10
  end
43
11
 
44
- def dump
45
- puts "#{self.class}"
46
- indent {
47
- puts "migration_dir: #{migration_dir}"
48
- puts "features_dir: #{features_dir}"
49
- puts "version: #{version}"
50
- puts "base_version: #{base_version}"
51
- }
12
+ def self.load
13
+ state = MigrationState.load
14
+ Migration.new(state.version, state.base_version)
52
15
  end
53
16
 
54
- # Needs to be redefined in ReleaseMigration. This default implementation
55
- # assumes #migration_dir is equal to #features_dir
56
- def self.load(directory) self.new(directory, directory).load end
57
-
58
- def load
59
- @migration_state&.read
60
- @features_state.read
61
- self
17
+ # Remove content of the migration/ directory
18
+ def self.clear
19
+ FileUtils.empty!(MIGRATION_DIR)
62
20
  end
63
21
 
64
- def save
65
- @migration_state&.write
66
- @features_state.write
67
- Git.add(@migration_state.path) if @migration_state
68
- Git.add(@features_state.path)
69
- self
70
- end
22
+ def exist?() MigrationState.exist? end
71
23
 
72
- # Return true if this is the initial 0.0.0 migration
73
- def initial?() version&.zero? end
74
-
75
- def exist?()
76
- (@migration_state.nil? || @migration_state.exist?) && (initial? || @features_state.exist?)
77
- end
78
-
79
- def create(templates_pattern = nil)
80
- if @migration_state && !@migration_state.exist?
81
- FileUtils.mkdir_p(migration_dir) if migration_dir
82
- @migration_state.create
83
- Git.add(@migration_state.path)
84
- end
85
- if !initial? && !@features_state.exist?
86
- FileUtils.mkdir_p(features_dir)
87
- @features_state.create
88
- Git.add(@features_state.path)
89
- end
90
- if !initial? && templates_pattern
91
- files = Share.cp(templates_pattern, features_dir, clobber: false)
92
- Git.add files
93
- end
24
+ def create()
25
+ files = Share.cp "migration/*", MIGRATION_DIR
26
+ state = MigrationState.new.write(version: version, base_version: base_version)
27
+ Git.add files, state.path
94
28
  self
95
29
  end
96
30
 
97
- def insert_feature(version)
98
- features.unshift(version.to_s)
99
- @features_state.write
100
- Git.add(@features_state.path)
101
- generate_features_yml
31
+ def update(version)
32
+ state = MigrationState.new.read
33
+ @version = state.version = version
34
+ state.write
35
+ Git.add state.path
102
36
  self
103
37
  end
104
38
 
105
- def append_feature(version)
106
- features.push(version.to_s)
107
- @features_state.write
108
- Git.add(@features_state.path)
109
- generate_features_yml
110
- self
111
- end
112
-
113
- def generate_features_yml
114
- features_yml_file = File.join(features_dir, "features.yml")
115
- Share.cp("release_migration/features.yml", features_yml_file)
116
- file = File.open(features_yml_file, "a")
117
- features.each { |feature|
118
- version = Version.new(feature)
119
- if base_version == version.truncate(:pre)
120
- file.puts "- #{version.feature}"
121
- else
122
- file.puts "- ../#{version.truncate(:pre)}/#{version.feature}"
123
- end
124
- }
125
- end
126
-
127
- def migrate(database)
128
- MigrationBuilder.new(database, features_dir).build
129
- Rdbms.exec_sql(database.name, "delete from prick.versions", user: database.user)
130
- Rdbms.exec_file(database.name, File.join(PRICK_DIR, "data.sql"), user: database.user)
131
- end
132
-
133
- # This only migrates included features. It is used in pre-releases to migrate included features
134
- # before creating a diff, so the diff only contains changes not defined in the features
135
- def migrate_features(database)
136
- MigrationBuilder.new(database, features_dir).build("features.yml")
39
+ def migrate(database)
40
+ base_version or raise Internal, "Can't migrate from nil to #{version}"
41
+ version or raise Internal, "Can't migrate from #{base_version} to nil"
42
+ MigrationBuilder.new(database, MIGRATION_DIR).build
137
43
  end
138
44
  end
139
45
 
140
- class ReleaseMigration < Migration
141
- def version() @migration_state&.version end
142
- def version=(v)
143
- @migration_state ||= MigrationState.new(ReleaseMigration.migration_dir(v))
144
- @migration_state.version = v
145
- end
46
+ class FeatureMigration < Migration
47
+ attr_reader :feature
146
48
 
147
- def base_version() @migration_state&.base_version end # || @migration_state.version end
148
- def base_version=(v)
149
- @migration_state or raise Internal, "Can't set base version when version is undefined"
150
- @migration_state.base_version = v
49
+ def initialize(feature, base_version)
50
+ super(base_version, base_version)
51
+ @feature = feature
151
52
  end
152
53
 
153
- # Note that `version` can be nil
154
- def initialize(version, base_version)
155
- migration_dir = version && ReleaseMigration.migration_dir(version)
156
- features_dir = ReleaseMigration.features_dir(base_version)
157
- super(migration_dir, features_dir)
158
- if version
159
- self.version = version
160
- self.base_version = base_version
161
- else
162
- @base_migration_state = MigrationState.new(features_dir)
163
- end
54
+ def self.load
55
+ migration_state = MigrationState.load
56
+ feature_state = FeatureState.load
57
+ FeatureMigration.new(feature_state.feature, migration_state.base_version)
164
58
  end
165
59
 
166
- def create() super("release_migration/*") end
167
-
168
- def self.load(migration_dir, features_dir = nil)
169
- !migration_dir.nil? or raise "Parameter migration_dir can't be nil"
170
- migration_state = MigrationState.new(migration_dir).read
171
- if features_dir.nil?
172
- features_dir = ReleaseMigration.features_dir(migration_state.base_version)
173
- end
174
- self.new(migration_state.version, migration_state.base_version)
175
- end
176
-
177
- def self.migration_dir(version) version && File.join(RELEASES_DIR, version.to_s) end
178
- def self.features_dir(base_version) migration_dir(base_version) end
179
-
180
-
181
- end
182
-
183
- class MigrationMigration < Migration
184
- attr_reader :name
185
-
186
- def initialize(version, base_version)
187
- @name = "#{base_version}_#{version}"
188
- dir = File.join(MIGRATIONS_DIR, name)
189
- super(dir, dir)
190
- self.version = version
191
- self.base_version = base_version
60
+ def create()
61
+ super
62
+ files = Share.cp "migration", File.join(MIGRATION_DIR, feature)
63
+ state = FeatureState.write(feature: feature)
64
+ Git.add files, state.path
65
+ self
192
66
  end
193
-
194
- def create() super("release_migration/*") end
195
67
  end
68
+ end
196
69
 
197
- class FeatureMigration < Migration
198
- attr_reader :name
199
-
200
- def initialize(name, base_version)
201
- @name = name
202
- dir = File.join(RELEASES_DIR, base_version.to_s, name)
203
- super(dir, dir)
204
- self.version = Version.new(base_version, feature: name)
205
- self.base_version = base_version
206
- end
207
70
 
208
- def create() super("feature_migration/*") end
209
- end
210
- end
@@ -3,6 +3,205 @@ 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 != File.basename(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_cache
50
+ project.cache.list.each { |l| puts l }
51
+ end
52
+
53
+ def build(database, version, nocache)
54
+ check_owned(database) if database
55
+ into_mesg = database && "into #{database}"
56
+ version_mesg = version ? "v#{version}" : "current schema"
57
+ version &&= Version.new(version)
58
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
59
+ database = database ? Database.new(database, project.user) : project.database(version)
60
+ project.build(database, version: version)
61
+ project.save(database) if version && !nocache
62
+ mesg "Built", version_mesg, into_mesg
63
+ end
64
+
65
+ def make(database, version, nocache)
66
+ check_owned(database) if database
67
+ version &&= Version.new(version)
68
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
69
+ if !nocache && version && project.cache.exist?(version)
70
+ load(database, version)
71
+ else
72
+ build(database, version, nocache)
73
+ end
74
+ end
75
+
76
+ def make_clean(all)
77
+ project.cache.clean.each { |file| puts "Removed cache file #{File.basename(file)}" }
78
+ drop(nil, all)
79
+ end
80
+
81
+ def load(database, file_or_version)
82
+ check_owned(database) if database
83
+ into_mesg = database && "into #{database}"
84
+ if version = Version.try(file_or_version)
85
+ database = database ? Database.new(database, project.user) : project.database(version)
86
+ project.load(database, version: version)
87
+ mesg "Loaded v#{version}", into_mesg, "from cache"
88
+ else file = file_or_version
89
+ project.load(database, file: file)
90
+ mesg "Loaded #{File.basename(file)}", into_mesg
91
+ end
92
+ end
93
+
94
+ def save(version, file)
95
+ database = project.database(version)
96
+ database.exist? or raise "Can't find database '#{database}'"
97
+ subj_mesg = file ? file : "cache"
98
+ project.save(database, file: file)
99
+ mesg "Saved #{database} to #{subj_mesg}"
100
+ end
101
+
102
+ def drop(database, all)
103
+ database.nil? || !all or raise Error, "Can't use --all when database is given"
104
+ if database
105
+ check_owned(database)
106
+ dbs = [database]
107
+ else
108
+ dbs = Rdbms.list_databases(Prick.database_re(project.name))
109
+ dbs << project.name if all && project.database.exist?
110
+ end
111
+ dbs += Rdbms.list_databases(Prick.tmp_databases_re(project.name)) # FIXME: Only used in dev
112
+ dbs.each { |db|
113
+ Rdbms.drop_database(db)
114
+ mesg "Dropped database #{db}"
115
+ }
116
+ end
117
+
118
+ # `select` is a tri-state variable that can be :tables, :no_tables, or :all
119
+ # (or any other value)
120
+ def diff(from, to, mark, select)
121
+ diff = project.diff(from && Version.new(from), to && Version.new(to))
122
+ if !diff.same?
123
+ if select != :tables && !diff.before_table_changes.empty?
124
+ puts "-- BEFORE TABLE CHANGES" if mark
125
+ puts diff.before_table_changes
126
+ end
127
+ if select != :no_tables && !diff.table_changes.empty?
128
+ puts "-- TABLE CHANGES" if mark
129
+ puts diff.table_changes
130
+ end
131
+ if select != :tables && !diff.after_table_changes.empty?
132
+ puts "-- AFTER TABLE CHANGES" if mark
133
+ puts diff.after_table_changes
134
+ end
135
+ end
136
+ end
137
+
138
+ def prepare_release(fork)
139
+ project.prepare_release(fork)
140
+ end
141
+
142
+ def prepare_diff(version = nil)
143
+ # Helpful check to ensure the user has built the current version
144
+ # Diff.same?(to_db, database) or
145
+ # raise Error, "Schema and project database are not synchronized"
146
+
147
+ project.prepare_diff(version || project.version)
148
+ if FileUtils.compare_file(TABLES_DIFF_PATH, TABLES_DIFF_SHARE_PATH)
149
+ mesg "Created diff. No table changes found, no manual migration updates needed"
150
+ else
151
+ mesg "Created diff. Please inspect #{TABLES_DIFF_PATH} and incorporate the"
152
+ mesg "changes in #{MIGRATION_DIR}/migrate.sql"
153
+ end
154
+ end
155
+
156
+ def create_release(version = nil)
157
+ if project.prerelease_branch?
158
+ raise NotYet
159
+ elsif project.release_branch?
160
+ project.create_release(Version.new(version))
161
+ else
162
+ raise Error, "You need to be on a release or pre-release branch to create a new release"
163
+ end
164
+ mesg "Created release v#{project.head.version}"
165
+ end
166
+
167
+ def generate_schema
168
+ project.generate_schema
169
+ end
170
+
171
+ def generate_migration
172
+ project.generate_migration
173
+ end
174
+
175
+ # TODO: Create a Backup class and a Project#backup_store object
176
+ def backup(file = nil)
177
+ file = file || File.join(SPOOL_DIR, "#{project.name}-#{Time.now.utc.strftime("%Y%m%d-%H%M%S")}.sql.gz")
178
+ project.save(file: file)
179
+ mesg "Backed-up database to #{file}"
180
+ end
181
+
182
+ def restore(file_arg = nil)
183
+ file = file_arg || Dir.glob(File.join(SPOOL_DIR, "#{name}-*.sql.gz")).sort.last
184
+ File.exist?(file) or raise Error, "Can't find #{file_arg || "any backup file"}"
185
+ project.load(file: file)
186
+ mesg "Restored database from #{file}"
187
+ end
188
+
189
+ protected
190
+ def check_owned(database)
191
+ database =~ ALL_DATABASES_RE or raise Error, "Not a prick database: #{database}"
192
+ project_name = $1
193
+ project_name == project.name or raise Error, "Database not owned by this prick project: #{database}"
194
+ end
195
+
196
+ def mesg(*args) puts args.compact.grep(/\S/).join(' ') if !quiet end
197
+ def verb(*args) puts args.compact.grep(/\S/).join(' ') if verbose end
198
+ end
199
+ end
200
+
201
+ __END__
202
+
203
+
204
+ module Prick
6
205
  class Program
7
206
  def project() @project ||= Project.load end
8
207