prick 0.2.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.
@@ -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
+