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