prick 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ require 'fcntl'
2
+
3
+ module Command
4
+ class Error < RuntimeError
5
+ attr_reader :status
6
+ attr_reader :stdout
7
+ attr_reader :stderr
8
+
9
+ def initialize(message, status, stdout, stderr)
10
+ super(message)
11
+ @status = status
12
+ @stdout = stdout
13
+ @stderr = stderr
14
+ end
15
+ end
16
+
17
+ # Execute the shell command 'cmd' and return the output as an array of
18
+ # strings
19
+ #
20
+ # If :stderr is true, it returns return a tuple of [stdout, stderr] instead
21
+ #
22
+ # The shell command is executed with the `errexit` and `pipefail` bash
23
+ # options. It raises a Command::Error exception if the command fails unless
24
+ # :fail is true. The exit status of the last command is stored in ::status
25
+ #
26
+ def command(cmd, stderr: false, fail: true)
27
+ cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
28
+
29
+ pw = IO::pipe # pipe[0] for read, pipe[1] for write
30
+ pr = IO::pipe
31
+ pe = IO::pipe
32
+
33
+ STDOUT.flush
34
+
35
+ pid = fork {
36
+ pw[1].close
37
+ pr[0].close
38
+ pe[0].close
39
+
40
+ STDIN.reopen(pw[0])
41
+ pw[0].close
42
+
43
+ STDOUT.reopen(pr[1])
44
+ pr[1].close
45
+
46
+ STDERR.reopen(pe[1])
47
+ pe[1].close
48
+
49
+ exec(cmd)
50
+ }
51
+
52
+ pw[0].close
53
+ pr[1].close
54
+ pe[1].close
55
+
56
+ @status = Process.waitpid2(pid)[1].exitstatus
57
+
58
+ out = pr[0].readlines.collect { |line| line.chop }
59
+ err = pe[0].readlines.collect { |line| line.chop }
60
+
61
+ pw[1].close
62
+ pr[0].close
63
+ pe[0].close
64
+
65
+ if @status == 0 || fail == false
66
+ stderr ? [out, err] : out
67
+ else
68
+ raise Command::Error.new((out + err).join("\n") + "\n", status, out, err)
69
+ end
70
+ end
71
+
72
+ # Exit status of the last command
73
+ def status() @status end
74
+
75
+ # Like command but returns true if the command exited with the expected status
76
+ def command?(cmd, expect: 0)
77
+ command(cmd, fail: false)
78
+ @status == expect
79
+ end
80
+
81
+ module_function :command
82
+ module_function :status
83
+ module_function :command?
84
+ end
85
+
@@ -0,0 +1,199 @@
1
+
2
+ module Prick
3
+ # Shared files (part of the installation)
4
+ SHARE_PATH = "#{File.dirname(File.dirname(__dir__))}/share"
5
+
6
+ # Project directories
7
+ DIRS = [
8
+ RELEASE_DIR = "releases",
9
+ MIGRATION_DIR = "migrations",
10
+ FEATURE_DIR = "features",
11
+ SCHEMA_DIR = "schemas",
12
+ PRICK_DIR = "#{SCHEMA_DIR}/prick",
13
+ PUBLIC_DIR = "#{SCHEMA_DIR}/public",
14
+ VAR_DIR = "var",
15
+ CACHE_DIR = "#{VAR_DIR}/cache",
16
+ TMP_DIR = "tmp",
17
+ CLONE_DIR = "tmp/clone",
18
+ SPEC_DIR = "spec"
19
+ ]
20
+
21
+ # Dump files
22
+ DUMP_EXT = "dump.gz"
23
+ DUMP_GLOB = "*-[0-9]*.#{DUMP_EXT}"
24
+ def self.dump_glob(project_name) "#{project_name}-*.#{DUMP_EXT}" end
25
+
26
+
27
+ # Matches an identifier. Identifiers consist of lower case letters, digits
28
+ # and underscores but not dashes because they're used as separators
29
+ IDENT_SUB_RE = /[a-z][a-z0-9_]*/
30
+ IDENT_RE = /^#{IDENT_SUB_RE}$/
31
+
32
+ # Matches an uppercase identifier
33
+ # UPCASE_IDENT_SUB_RE = /[A-Z][A-Z0-9_]*/
34
+ # UPCASE_IDENT_RE = /#{UPCASE_IDENT_SUB_RE}/
35
+
36
+ # A (system) name. Names are used for projects and usernames that are
37
+ # external to prick and can include both dashes and underscores but dashes
38
+ # have to be followed by a character and not a digit so 'ident-1234' is not
39
+ # allowed but 'ident_1234' and 'ident1234' are
40
+ NAME_SUB_RE = /#{IDENT_SUB_RE}/
41
+ NAME_RE = /^#{NAME_SUB_RE}$/
42
+
43
+ # Matches a project name
44
+ PROJECT_NAME_SUB_RE = NAME_SUB_RE
45
+ PROJECT_NAME_RE = NAME_RE
46
+
47
+ # Matches a custom name
48
+ CUSTOM_NAME_SUB_RE = NAME_SUB_RE
49
+ CUSTOM_NAME_RE = NAME_RE
50
+
51
+ # Matches a feature name
52
+ FEATURE_NAME_SUB_RE = NAME_SUB_RE
53
+ FEATURE_NAME_RE = NAME_RE
54
+
55
+ # Matches a postgres user name
56
+ USER_NAME_SUB_RE = NAME_SUB_RE
57
+ USER_NAME_RE = NAME_RE
58
+
59
+ # Matches a major.minor.patch version
60
+ #
61
+ # The *_SEMVER REs are derived from the canonical RE
62
+ #
63
+ # /
64
+ # (0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)
65
+ # (?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?
66
+ # (?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
67
+ # /x
68
+ #
69
+ # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.
70
+ #
71
+ MMP_SEMVER_SUB_RE = /(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)/
72
+ MMP_SEMVER_RE = /^#{MMP_SEMVER_SUB_RE}$/
73
+
74
+ # Matches the prelease part of a semantic version
75
+ #
76
+ PRE_SEMVER_SUB_RE = /pre\.\d+/
77
+ PRE_SEMVER_RE = /^#{PRE_SEMVER_SUB_RE}$/
78
+
79
+ # Matches a semantic version
80
+ #
81
+ SEMVER_SUB_RE = /#{MMP_SEMVER_SUB_RE}(?:-#{PRE_SEMVER_SUB_RE})?/
82
+ SEMVER_RE = /^#{SEMVER_SUB_RE}$/
83
+
84
+ # Version RE. The general syntax for a version is '<custom>-<version>_<feature>'
85
+ #
86
+ # The RE defines the following captures:
87
+ # $1 - custom name, can be nil
88
+ # $2 - semantic version
89
+ # $3 - feature name, can be nil
90
+ #
91
+ VERSION_SUB_RE = /
92
+ (?:(#{CUSTOM_NAME_SUB_RE})-)?
93
+ (#{SEMVER_SUB_RE})
94
+ (?:_(#{FEATURE_NAME_SUB_RE}))?
95
+ /x
96
+ VERSION_RE = /^#{VERSION_SUB_RE}$/
97
+
98
+ # Matches an abstract release (either a release or a prerelease)
99
+ #
100
+ # The RE defines the following captures:
101
+ # $1 - custom name, can be nil
102
+ # $2 - semantic version
103
+ #
104
+ ABSTRACT_RELEASE_SUB_RE = /
105
+ (?:(#{CUSTOM_NAME_SUB_RE})-)?
106
+ (#{SEMVER_SUB_RE})
107
+ /x
108
+ ABSTRACT_RELEASE_RE = /^#{ABSTRACT_RELEASE_SUB_RE}$/
109
+
110
+ # Matches a (proper) release
111
+ #
112
+ # The RE defines the following captures:
113
+ # $1 - custom name, can be nil
114
+ # $2 - semantic version
115
+ #
116
+ RELEASE_SUB_RE = /
117
+ (?:(#{CUSTOM_NAME_SUB_RE})-)?
118
+ (#{MMP_SEMVER_SUB_RE})
119
+ /x
120
+ RELEASE_RE = /^#{RELEASE_SUB_RE}$/
121
+
122
+ # Migration RE. The syntax is <version>_<version>
123
+ #
124
+ # The RE defines the following captures
125
+ # $1 - from version
126
+ # $2 - from custom name, can be nil
127
+ # $3 - from semantic version
128
+ # $4 - to version
129
+ # $5 - to custom name, can be nil
130
+ # $6 - to semantic version
131
+ #
132
+ MIGRATION_SUB_RE = /(#{RELEASE_SUB_RE})_(#{RELEASE_SUB_RE})/
133
+ MIGRATION_RE = /^#{MIGRATION_SUB_RE}$/
134
+
135
+ # Matches a prerelease branch
136
+ #
137
+ # The RE defines the following captures:
138
+ # $1 - custom name, can be nil
139
+ # $2 - semantic version
140
+ #
141
+ PRERELEASE_SUB_RE = /
142
+ (?:(#{CUSTOM_NAME_SUB_RE})-)?
143
+ (#{MMP_SEMVER_SUB_RE}-#{PRE_SEMVER_SUB_RE})
144
+ /x
145
+ PRERELEASE_RE = /^#{PRERELEASE_SUB_RE}$/
146
+
147
+ # Matches a feature branch
148
+ #
149
+ # The RE defines the following captures:
150
+ # $1 - custom name, can be nil
151
+ # $2 - semantic version
152
+ # $3 - feature name
153
+ #
154
+ FEATURE_SUB_RE = /#{ABSTRACT_RELEASE_SUB_RE}_(#{FEATURE_NAME_SUB_RE})/
155
+ FEATURE_RE = /^#{FEATURE_SUB_RE}$/
156
+
157
+ # Project release RE. The general syntax is '<project>-<custom>-<version>'
158
+ #
159
+ # The RE defines the following captures:
160
+ # $1 - project
161
+ # $2 - version
162
+ # $3 - custom name, can be nil
163
+ # $4 - semantic version
164
+ #
165
+ PROJECT_SUB_RE = /(#{PROJECT_NAME_SUB_RE})-(#{ABSTRACT_RELEASE_SUB_RE})/
166
+ PROJECT_RE = /^#{PROJECT_SUB_RE}$/
167
+ def self.project_sub_re(project_name)
168
+ /(#{Regexp.escape(project_name)})(-#{VERSION_SUB_RE})/
169
+ end
170
+ def self.release_re(project_name) /^#{project_sub_re(project_name)}$/ end
171
+
172
+ # Matches versioned databases. Note that databases never include the feature name.
173
+ # Features use the project database instead of a feature-specific database
174
+ #
175
+ # The RE defines the following captures
176
+ # $1 - project
177
+ # $2 - version
178
+ # $3 - custom name, can be nil
179
+ # $4 - semantic version
180
+ #
181
+ DATABASE_SUB_RE = PROJECT_SUB_RE
182
+ DATABASE_RE = /^#{DATABASE_SUB_RE}$/
183
+ def self.database_sub_re(project_name) project_sub_re(project_name) end
184
+ def self.database_re(project_name) /^#{database_sub_re(project_name)}$/ end
185
+
186
+ # Matches project database and versioned databases
187
+ #
188
+ # The RE defines the following captures
189
+ # $1 - project
190
+ # $2 - version, can be nil
191
+ # $3 - custom name, can be nil
192
+ # $4 - semantic version, can be nil
193
+ #
194
+ ALL_DATABASES_SUB_RE = /(#{PROJECT_NAME_SUB_RE})(?:-(#{ABSTRACT_RELEASE_SUB_RE}))?/
195
+ ALL_DATABASES_RE = /^#{ALL_DATABASES_SUB_RE}$/
196
+ def self.all_databases_sub_re(project_name) /(#{project_name})(?:-(#{ABSTRACT_RELEASE_SUB_RE}))?/ end
197
+ def self.all_databases_re(project_name) /^#{all_databases_sub_re(project_name)}$/ end
198
+ end
199
+
@@ -0,0 +1,58 @@
1
+
2
+ require "prick/ensure.rb"
3
+
4
+ module Prick
5
+ class Database
6
+ attr_reader :name
7
+ attr_reader :user
8
+
9
+ def initialize(name, user)
10
+ @name = name
11
+ @user = user
12
+ @version = nil
13
+ end
14
+
15
+ def version
16
+ @version ||= begin
17
+ version =
18
+ if exist? && Rdbms.exist_table?(name, "prick", "versions")
19
+ Rdbms.select(name, "select version from prick.versions")&.first&.first
20
+ else
21
+ nil
22
+ end
23
+ version && Version.new(version)
24
+ end
25
+ end
26
+
27
+ def version=(version)
28
+ @version = version && Version.new(version)
29
+ Rdbms.exec_sql(name, %(
30
+ update prick.versions
31
+ set major = '#{@version.major}',
32
+ minor = '#{@version.minor}',
33
+ patch = '#{@version.patch}',
34
+ pre = '#{@version.pre}',
35
+ version = '#{@version.to_s}'
36
+ )) if @version
37
+ @version
38
+ end
39
+
40
+ def exist?() Rdbms.exist_database?(name) end
41
+ def create() Rdbms.create_database(name, owner: user) end
42
+ def recreate() drop if exist?; create; @version = nil end
43
+ def drop() Rdbms.drop_database(name, fail: false); @version = nil end
44
+
45
+ def loaded?() exist? && !version.nil? end
46
+ def load(file) Rdbms.load(name, file, user: user); @version = nil end
47
+
48
+ def save(file) Rdbms.save(name, file); @version = nil end
49
+
50
+ include Ensure
51
+
52
+ private
53
+ @states = {
54
+ exist: [:create, :drop],
55
+ loaded: [:exist, :load, :recreate]
56
+ }
57
+ end
58
+ end
@@ -0,0 +1,151 @@
1
+ require 'tsort'
2
+
3
+ module DSort
4
+ # Thrown if a cyclic dependency is detected
5
+ #
6
+ # DSort::Cyclic is inherited from TSort::Cyclic so that recue handling code
7
+ # written for TSort will still work. It provides a #cycles member that lists
8
+ # the cycles
9
+ class Cyclic < TSort::Cyclic
10
+ # List of detected cycles sorted from shortest to longest cycle
11
+ attr_reader :cycles
12
+
13
+ def initialize(dsort_object)
14
+ @cycles =
15
+ dsort_object.each_strongly_connected_component \
16
+ .select { |e| e.size > 1 } \
17
+ .sort { |l,r| r <=> l }
18
+ gram = cycles.size > 1 ? "ies" : "y"
19
+ super("Cyclic depedendenc#{gram} detected")
20
+ end
21
+ end
22
+
23
+ # dsort sorts its input in "dependency" order: The input can be thought of
24
+ # depends-on relations between objects and the output as sorted in the order
25
+ # needed to safisfy those dependencies so that no object comes before an
26
+ # object it depends on
27
+ #
28
+ # dsort can take an array or a hash argument, or be supplied with a block
29
+ #
30
+ # The Array argument should consist of pairs (two-element Arrays) with the
31
+ # first element being the depending object and the second an object or an
32
+ # array of objects it depends on: For example [:a, :b] means that :a depends
33
+ # on :b, and [:b, [:c, :d]] that :b depends on both :c and :d
34
+ #
35
+ # The Hash argument should be a hash from depending object to an object or
36
+ # array of objects it depends on. If h is a Hash then dsort(h) is equivalent
37
+ # to dsort(h.to_a)
38
+ #
39
+ # Note that if the elements are arrays themselves, then you should use the
40
+ # array form to list the dependencies even if there is only one dependency.
41
+ # Ie. use [:a, [:b]] or {:a => [:b] } instead of [:a, :b] or {:a => :b}
42
+ #
43
+ # If dsort is given a block, the block is given an element as argument and
44
+ # should return an (possibly empty) array of the objects the argument depends
45
+ # on. The argument to dsort should be an element or an array of elements to
46
+ # be given to the block. Note that if the elements are arrays themselves,
47
+ # then the arguments to dsort should use the array form even if there is only
48
+ # one element. Ie. Use dsort([:a]) instead of dsort(:a)
49
+ #
50
+ # dsort raise a DSort::Cyclic exception if a cycle detected
51
+ #
52
+ # Example: If we have that dsort depends on ruby and rspec, ruby depends
53
+ # on C to compile, and rspec depends on ruby, then in what order should we
54
+ # build them ? Using dsort we could do
55
+ #
56
+ # p dsort [[:dsort, [:ruby, :rspec]]], [:ruby, :C], [:rspec, :ruby]]
57
+ # => [:C, :ruby, :rspec, :dsort]
58
+ #
59
+ # Using a hash
60
+ #
61
+ # h = {
62
+ # :dsort => [:ruby, :rspec],
63
+ # :ruby => [:C],
64
+ # :rspec => [:ruby]
65
+ # }
66
+ # p dsort(h) # Same as dsort(h.to_a)
67
+ # => [:C, :ruby, :rspec, :dsort]
68
+ #
69
+ # or using a block
70
+ #
71
+ # p dsort(:dsort) { |e| h[e] }
72
+ # => [:C, :ruby, :rspec, :dsort]
73
+ #
74
+ def dsort(a, &block)
75
+ sort_object = DSortPrivate::DSortObject.new(a, &block)
76
+ begin
77
+ sort_object.tsort
78
+ rescue TSort::Cyclic
79
+ raise Cyclic.new(sort_object)
80
+ end
81
+ end
82
+
83
+ # tsort sort its input in topological order: The input can be thought of as
84
+ # comes-before relations between objects and the output will be in
85
+ # first-to-last order. This definition corresponds to the mathemacial
86
+ # defitionnn of topological sort. See
87
+ # http://en.wikipedia.org/wiki/Topological_sorting
88
+ #
89
+ # Arguments are the same as for dsort. tsort is equivalent to
90
+ # dsort(...).reverse
91
+ #
92
+ # tsort raise a DSort::Cyclic exception if a cycle is detected (DSort::Cyclic
93
+ # is an alias for TSort::Cyclic)
94
+ #
95
+ def tsort(a, &block) dsort(a, &block).reverse end
96
+
97
+ module_function :dsort, :tsort
98
+
99
+ module DSortPrivate
100
+ class DSortObject
101
+ include TSort
102
+
103
+ # Hash from element to array of dependencies
104
+ attr_reader :deps
105
+
106
+ # Create @deps hash from object to list of dependencies
107
+ def initialize(a, &block)
108
+ @deps = {}
109
+ if block_given?
110
+ a = [a] if !a.is_a?(Array)
111
+ a.each { |elem| find_dependencies(elem, &block) }
112
+ else
113
+ a.each { |obj, deps|
114
+ (@deps[obj] ||= []).concat(deps.is_a?(Array) ? deps : [deps])
115
+ }
116
+ end
117
+
118
+ # Make sure all dependent objects are also represented as depending
119
+ # objects: If we're given [:a, :b] we want the @deps hash to include
120
+ # both :a and :b as keys
121
+ @deps.values.each { |deps|
122
+ (deps.is_a?(Array) ? deps : [deps]).each { |d|
123
+ @deps[d] = [] if !@deps.key?(d)
124
+ }
125
+ }
126
+ end
127
+
128
+ # TSort virtual methods
129
+ def tsort_each_node(&block) @deps.each_key(&block) end
130
+ def tsort_each_child(node, &block) @deps[node].each(&block) end
131
+
132
+ private
133
+ def find_dependencies(a, &block)
134
+ block.call(a).each { |d|
135
+ (@deps[a] ||= []) << d
136
+ find_dependencies(d, &block)
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+