prick 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -5
  3. data/Gemfile +4 -1
  4. data/TODO +3 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +224 -370
  7. data/lib/ext/fileutils.rb +11 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +17 -9
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +141 -0
  13. data/lib/prick/command.rb +19 -11
  14. data/lib/prick/constants.rb +42 -20
  15. data/lib/prick/database.rb +5 -3
  16. data/lib/prick/diff.rb +47 -0
  17. data/lib/prick/exceptions.rb +15 -3
  18. data/lib/prick/git.rb +46 -21
  19. data/lib/prick/migration.rb +165 -185
  20. data/lib/prick/program.rb +238 -0
  21. data/lib/prick/project.rb +266 -358
  22. data/lib/prick/rdbms.rb +2 -2
  23. data/lib/prick/schema.rb +19 -88
  24. data/lib/prick/share.rb +64 -0
  25. data/lib/prick/state.rb +137 -0
  26. data/lib/prick/version.rb +34 -14
  27. data/libexec/strip-comments +33 -0
  28. data/make_releases +48 -345
  29. data/make_schema +10 -0
  30. data/prick.gemspec +11 -22
  31. data/share/feature_migration/diff.sql +2 -0
  32. data/share/feature_migration/migrate.sql +2 -0
  33. data/share/release_migration/diff.sql +3 -0
  34. data/share/release_migration/features.yml +6 -0
  35. data/share/release_migration/migrate.sql +5 -0
  36. data/share/release_migration/migrate.yml +5 -0
  37. data/share/schema/build.yml +14 -0
  38. data/share/schema/schema.sql +5 -0
  39. data/share/schemas/build.yml +3 -0
  40. data/share/schemas/prick/build.yml +14 -0
  41. data/share/schemas/prick/data.sql +1 -2
  42. data/share/schemas/prick/schema.sql +0 -15
  43. data/share/schemas/prick/tables.sql +17 -0
  44. data/share/schemas/public/.keep +0 -0
  45. data/share/schemas/public/build.yml +14 -0
  46. data/share/schemas/public/schema.sql +3 -0
  47. data/test_assorted +192 -0
  48. data/test_feature +112 -0
  49. data/test_single_dev +83 -0
  50. metadata +34 -61
@@ -1,11 +1,11 @@
1
1
  require 'prick/command.rb'
2
- require 'prick/ensure.rb'
2
+ #require 'prick/ensure.rb'
3
3
 
4
4
  require 'csv'
5
5
 
6
6
  module Prick
7
7
  module Rdbms
8
- extend Ensure
8
+ # extend Ensure
9
9
 
10
10
  ### EXECUTE SQL
11
11
 
@@ -1,100 +1,31 @@
1
- require "prick/constants.rb"
2
- require "prick/exceptions.rb"
1
+ require "prick/state.rb"
3
2
 
4
3
  module Prick
5
- # TODO: Resolve dependencies by extracting \i includes and then execute them
6
- # in topological order. This means the must be no 'if' constructs in the
7
- # files because otherwise dependencies are not static. The rule is that if a
8
- # file \i includes another file, then the included file can always be execute
9
- # before the current file
10
- #
11
- # Alternatively add some special tags:
12
- #
13
- # -- require prick (meaning include the prick schema)
14
- # -- require public/user-schema (include this file)
15
- #
16
- class Schema
17
- # Enclosing project
18
- attr_reader :project
19
-
20
- # Path to data file
21
- def Schema.data_file() DATA_SQL_PATH end
22
- def data_file() Schema.data_file end
23
-
24
- # Version read from schemas/prick/data.sql
25
- def version()
26
- @version ||= begin
27
- File.open(data_file, "r") { |file|
28
- while !file.eof? && file.gets.chomp != COPY_STMT
29
- ;
30
- end
31
- !file.eof? or raise Fail, "No COPY statement in #{data_file}"
32
- l = file.gets.chomp
33
- a = l.split("\t")[1..].map { |val| val == '\N' ? nil : val }
34
- a.size == FIELDS.size or raise Fail, "Illegal data format in #{data_file}"
35
- custom, major, minor, patch, pre = a[0], *a[1..-2].map { |val| val && val.to_i }
36
- v = Version.new("0.0.0", custom: (custom == '\N' ? nil : custom))
37
- v.major = major
38
- v.minor = minor
39
- v.patch = patch
40
- v.pre = (pre == "null" ? nil : pre)
41
- v
42
- }
43
- end
44
- end
45
-
46
- # Write version number into schemas/prick/data.sql
47
- def version=(version)
48
- @version = Version.new(version)
49
- File.open(data_file, "w") { |f|
50
- f.puts "--"
51
- f.puts "-- This file is auto-generated by prick(1). Please don't touch"
52
- f.puts COPY_STMT
53
- f.print \
54
- "1\t",
55
- FIELDS[..-2].map { |f| @version.send(f.to_sym) || '\N' }.join("\t"),
56
- "\t#{@version}\n"
57
- f.puts "\\."
58
- }
59
- Git.add(data_file)
60
- @version
61
- end
4
+ PRICK_SCHEMA = "prick"
5
+ SCHEMA_NAMES = %w(schema roles types tables data constraints indexes views functions comments grants)
6
+ BUILD_BASE_NAME = "build"
7
+ BUILD_SQL_FILE = BUILD_BASE_NAME + ".sql"
8
+ BUILD_YML_FILE = BUILD_BASE_NAME + ".yml"
62
9
 
63
- def initialize(project)
64
- @project = project
65
- end
10
+ # Note this models the SCHEMAS_DIR directory, not a database schema
11
+ class Schema
12
+ attr_reader :directory
13
+ def yml_file() SchemaBuilder.yml_file(directory) end
66
14
 
67
- # Path to the schemas directory
68
- def path() "#{project.path}/#{SCHEMA_DIR}" end
15
+ def version() SchemaVersion.new(directory).read end
16
+ def version=(version) SchemaVersion.new(directory).write(version) end
17
+ def version_file() SchemaVersion.new(directory).path end
69
18
 
70
- def built?(database = project.database)
71
- database.exist? && database.version == version
19
+ def initialize(directory = SCHEMAS_DIR)
20
+ @directory = directory
72
21
  end
73
22
 
74
- # Build a release from the files in schemas/
75
- def build(database = project.database)
76
- files = collect("schema.sql") + collect("data.sql")
77
- Dir.chdir(path) {
78
- files.each { |file| Rdbms.exec_file(database.name, file, user: project.user) }
79
- }
80
- end
23
+ def built?(database) database.exist? && database.version == version end
81
24
 
82
- # Collects instances of `filename` in sub-directories of schemas/
83
- def collect(filename)
84
- Dir.chdir(path) {
85
- if File.exist?(filename)
86
- [filename]
87
- else
88
- Dir["*/#{filename}"]
89
- end
90
- }
25
+ # `subject` can be a subpath of schema/ (ie. 'public/tables')
26
+ def build(database, subject = nil)
27
+ SchemaBuilder.new(database, directory).build(subject)
91
28
  end
92
-
93
- private
94
- FIELDS = %w(custom major minor patch pre feature version)
95
- COPY_STMT = "COPY prick.versions (id, #{FIELDS.join(', ')}) FROM stdin;"
96
-
97
- DATA_SQL_PATH = "#{Prick::PRICK_DIR}/data.sql"
98
29
  end
99
30
  end
100
31
 
@@ -0,0 +1,64 @@
1
+
2
+ module Prick
3
+ class Share
4
+ # Procedural object for templating and copying share/ files
5
+ class Copier
6
+ attr_reader :clobber, :templates
7
+
8
+ def initialize(clobber, templates)
9
+ @clobber = clobber
10
+ @templates = templates
11
+ end
12
+
13
+ def cp(from, to)
14
+ if File.directory?(from)
15
+ cp_dir(from, to)
16
+ elsif File.file?(from)
17
+ cp_file(from, to)
18
+ else
19
+ raise Fail, "Can't copy #{from}"
20
+ end
21
+ end
22
+
23
+ def cp_file(from, to)
24
+ if clobber || !File.exist?(to)
25
+ if templates.empty?
26
+ FileUtils.copy_file(from, to)
27
+ else
28
+ File.open(to, "w") { |f|
29
+ File.readlines(from).each { |l|
30
+ templates.each { |key, value| l.gsub!(/\[<#{key}>\]/, value) }
31
+ f.puts l
32
+ }
33
+ }
34
+ end
35
+ [to]
36
+ else
37
+ []
38
+ end
39
+ end
40
+
41
+ def cp_dir(from, to)
42
+ FileUtils.mkdir_p(to)
43
+ Dir.children(from).map { |name|
44
+ cp(File.join(from, name), File.join(to, name))
45
+ }.flatten
46
+ end
47
+ end
48
+
49
+ def self.cp(pattern, dest, clobber: true, templates: {})
50
+ copier = Copier.new(clobber, templates)
51
+ matches = Dir.glob(File.join(SHARE_PATH, pattern))
52
+ if File.directory?(dest)
53
+ matches.map { |from| copier.cp(from, File.join(dest, File.basename(from))) }.flatten
54
+ elsif matches.size == 1
55
+ copier.cp(matches.first, dest)
56
+ elsif matches.size == 0
57
+ []
58
+ else
59
+ raise Internal, "Destination is not a directory: #{destdir}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,137 @@
1
+
2
+ require 'yaml'
3
+
4
+ module Prick
5
+ class PrickFile
6
+ attr_reader :path
7
+ def initialize(path) @path = path end
8
+ def exist?() File.exist?(path) end
9
+
10
+ protected
11
+ def general_read(method, branch: nil, tag: nil)
12
+ branch || tag ? Git.send(method, path, branch: branch, tag: tag) : File.open(path, "r").send(method)
13
+ end
14
+ def do_read(**opts) general_read(:read, **opts) end
15
+ def do_readlines(**opts) general_read(:readlines, **opts) end
16
+ end
17
+
18
+ class State < PrickFile
19
+ # `fields` is a Hash from field name (Symbol) to field type (class). Eg. { f: Integer }
20
+ def initialize(path, **fields)
21
+ super(path)
22
+ @fields = fields
23
+ @fields.each_key { |k| self.class.attr_accessor k }
24
+ end
25
+
26
+ def create() write end
27
+
28
+ def set(**fields)
29
+ for field, value in fields
30
+ self.send(:"#{field}=", value)
31
+ end
32
+ end
33
+
34
+ def read(branch: nil, tag: nil)
35
+ if branch.nil?
36
+ hash = YAML.load(do_read(branch: branch, tag: tag))
37
+ for field, klass in @fields
38
+ value = hash[field.to_s]
39
+ value = Version.new(value) if klass == Version && !value.nil?
40
+ self.instance_variable_set("@#{field}", value)
41
+ end
42
+ else
43
+ raise NotYet
44
+ end
45
+ self
46
+ end
47
+
48
+ def write
49
+ hash = @fields.map { |field, klass|
50
+ value = self.send(field)
51
+ value = value.to_s if klass == Version && !value.nil?
52
+ [field.to_s, value]
53
+ }.to_h
54
+ IO.write(@path, YAML.dump(hash))
55
+ raise if @version.is_a?(Array)
56
+ self
57
+ end
58
+ end
59
+
60
+ class MigrationState < State
61
+ def initialize(directory, version: nil, base_version: nil)
62
+ super(File.join(directory, MIGRATION_STATE_FILE), version: Version, base_version: Version)
63
+ set(version: version, base_version: base_version)
64
+ end
65
+ end
66
+
67
+ class FeaturesState < State
68
+ def initialize(directory, features: [])
69
+ super(File.join(directory, FEATURES_STATE_FILE), features: Array)
70
+ set(features: features)
71
+ end
72
+ end
73
+
74
+ class ProjectState < State
75
+ def initialize(name: nil, user: nil)
76
+ super(PROJECT_STATE_FILE, name: String, user: String)
77
+ set(name: name, user: user)
78
+ end
79
+ end
80
+
81
+ class PrickVersion < PrickFile
82
+ def initialize() super PRICK_VERSION_FILE end
83
+
84
+ def read(branch: nil, tag: nil)
85
+ Version.new(do_readlines(branch: branch, tag: tag).first.chomp.sub(/^prick-/, ""))
86
+ end
87
+
88
+ def write(version)
89
+ File.open(path, "w") { |file| file.puts "prick-#{version}" }
90
+ end
91
+ end
92
+
93
+ class SchemaVersion < PrickFile
94
+ def initialize(schema = SCHEMAS_DIR) super(File.join(schema, "prick", "data.sql")) end
95
+
96
+ def read(**opts)
97
+ lines = do_readlines
98
+ while !lines.empty?
99
+ line = lines.shift.chomp
100
+ next if line != COPY_STMT
101
+ l = lines.shift.chomp
102
+ a = l.split("\t")[1..].map { |val| val == '\N' ? nil : val }
103
+ a.size == FIELDS.size or raise Fail, "Illegal data format in #{path}"
104
+ custom, major, minor, patch, pre = a[0], *a[1..-2].map { |val| val && val.to_i }
105
+ v = Version.new("0.0.0", custom: (custom == '\N' ? nil : custom))
106
+ v.major = major
107
+ v.minor = minor
108
+ v.patch = patch
109
+ v.pre = (pre == "null" ? nil : pre)
110
+ return v
111
+ end
112
+ raise Fail, "No COPY statement in #{path}"
113
+ end
114
+
115
+ def write(version)
116
+ version_string = version.truncate(:pre).to_s
117
+ File.open(path, "w") { |f|
118
+ f.puts "--"
119
+ f.puts "-- This file is auto-generated by prick(1). Please don't touch"
120
+ f.puts COPY_STMT
121
+ f.print \
122
+ "1\t",
123
+ FIELDS[..-2].map { |f| version.send(f.to_sym) || '\N' }.join("\t"),
124
+ "\t#{version_string}\n"
125
+ f.puts "\\."
126
+ }
127
+ Git.add(path)
128
+ end
129
+
130
+ def create() raise Internal, "This should not happen" end
131
+
132
+ private
133
+ FIELDS = %w(custom major minor patch pre feature version)
134
+ COPY_STMT = "COPY prick.versions (id, #{FIELDS.join(', ')}) FROM stdin;"
135
+ end
136
+ end
137
+
@@ -1,15 +1,17 @@
1
1
 
2
- # Moved to lib/prick.rb to avoid having Gem depend on it
2
+ # "require 'semantic'" is moved to lib/prick.rb to avoid having Gem depend on it
3
3
  # require 'semantic' # https://github.com/jlindsey/semantic
4
4
 
5
5
  # Required by gem
6
6
  module Prick
7
- VERSION = "0.2.0"
7
+ VERSION = "0.3.0"
8
8
  end
9
9
 
10
10
  # Project related code starts here
11
11
  module Prick
12
12
  class Version
13
+ class FormatError < RuntimeError; end
14
+
13
15
  include Comparable
14
16
 
15
17
  PRE_LABEL = "pre"
@@ -18,9 +20,8 @@ module Prick
18
20
  def self.zero() Version.new("0.0.0") end
19
21
  def zero?() self == Version.zero end
20
22
 
21
- # Return true if `string` is a version. If true, it sets the Regex capture
22
- # groups 1-3. See also Constants::VERSION_RE
23
- def self.version?(string) (string =~ VERSION_RE).nil? ? false : true end
23
+ # Return true if `string` is a version
24
+ def self.version?(string) !(string =~ VERSION_RE).nil? end
24
25
 
25
26
  attr_accessor :custom
26
27
  attr_accessor :semver
@@ -41,8 +42,8 @@ module Prick
41
42
  # Return true if this is a feature release
42
43
  def feature?() !@feature.nil? end
43
44
 
44
- # Return true if this is a release branch
45
- def release?() !feature? end
45
+ # Return true if this is a release branch (and not a prerelease)
46
+ def release?() !feature? && !pre? end
46
47
 
47
48
  # Return true if this is a pre-release
48
49
  def pre?() !@semver.pre.nil? end
@@ -63,8 +64,7 @@ module Prick
63
64
  def initialize(version, custom: nil, feature: nil)
64
65
  case version
65
66
  when String
66
- version =~ VERSION_RE
67
- self.class.version?(version) or raise "Expected a version, got #{version.inspect}"
67
+ version =~ VERSION_RE or raise Version::FormatError, "Expected a version, got #{version.inspect}"
68
68
  @custom = custom || $1
69
69
  @semver = Semantic::Version.new($2)
70
70
  @feature = feature || $3
@@ -77,20 +77,28 @@ module Prick
77
77
  @semver = version.semver.dup
78
78
  @feature = feature || version.feature
79
79
  else
80
- raise "Expected a String, Version, or Semantic::Version, got #{version.class}"
80
+ raise Internal, "Expected a String, Version, or Semantic::Version, got #{version.class}"
81
81
  end
82
82
  end
83
83
 
84
+ # Try converting the string `version` into a Version object. Return nil if unsuccessful
85
+ def self.try(version)
86
+ version?(version) ? new(version) : nil
87
+ end
88
+
84
89
  # `part` can be one of :major, :minor, :patch, or :pre. If pre is undefined, it
85
90
  # is set to `pre_initial_value`
86
91
  def increment(part, pre_initial_value = 1)
92
+ self.dup.increment!(part, pre_initial_value)
93
+ end
94
+
95
+ def increment!(part, pre_initial_value = 1)
87
96
  if part == :pre
88
- v = self.dup
89
- v.pre = (v.pre ? v.pre+1 : pre_initial_value)
90
- v
97
+ self.pre = (self.pre ? self.pre + 1 : pre_initial_value)
91
98
  else
92
- Version.new(semver.increment!(part), custom: custom, feature: feature)
99
+ @semver = semver.increment!(part)
93
100
  end
101
+ self
94
102
  end
95
103
 
96
104
  def truncate(part)
@@ -109,6 +117,18 @@ module Prick
109
117
  end
110
118
  end
111
119
 
120
+ def tag() "v#{to_s}" end
121
+
122
+ def path
123
+ parts = [FEATURE_DIR, truncate(:pre), feature].compact
124
+ File.join(*parts)
125
+ end
126
+
127
+ def link
128
+ !feature? or raise Internal, "Version #{to_s} is a feature, not a release"
129
+ File.join(RELEASE_DIR, to_s)
130
+ end
131
+
112
132
  def <=>(other)
113
133
  r = (custom || "") <=> (other.custom || "")
114
134
  return r if r != 0
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/bash
2
+
3
+ # NAME
4
+ # strip-comments - Remove comments from postgres files
5
+ #
6
+ # USAGE
7
+ # strip-comments [FILE]
8
+ #
9
+ # DESCRIPTION
10
+ # Remove comments and blank lines from standard intput or the given file and
11
+ # write the result on standard output
12
+ #
13
+ # REFERENCES
14
+ # https://stackoverflow.com/a/35708616/1745001
15
+ #
16
+
17
+ PROGRAM=$(basename $0)
18
+ USAGE="[FILE]"
19
+
20
+ function error() {
21
+ echo "$PROGRAM: $@"
22
+ echo "Usage: $PROGRAM $USAGE"
23
+ exit 1
24
+ } >&2
25
+
26
+ [ $# -le 1 ] || error "Illegal number of arguments"
27
+ FILE=$1
28
+
29
+ sed '/^\s*--/d' $FILE \
30
+ | sed 's/a/aA/g; s/__/aB/g; s/#/aC/g' \
31
+ | gcc -P -E -ansi - \
32
+ | sed 's/aC/#/g; s/aB/__/g; s/aA/a/g'
33
+