prick 0.4.0 → 0.5.0

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